big documentation update - all public functions and classes are documented now
This commit is contained in:
parent
dc22a79ac4
commit
a9b610479d
10
.esdoc.json
Normal file
10
.esdoc.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"source": "./src",
|
||||||
|
"destination": "./docs",
|
||||||
|
"plugins": [{
|
||||||
|
"name": "esdoc-standard-plugin",
|
||||||
|
"option": {
|
||||||
|
"accessor": {"access": ["public"], "autoPrivate": true}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
bower_components
|
bower_components
|
||||||
|
docs
|
||||||
/y.*
|
/y.*
|
||||||
/examples/yjs-dist.js*
|
/examples/yjs-dist.js*
|
||||||
|
43
documentation.yml
Normal file
43
documentation.yml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
toc:
|
||||||
|
- Y
|
||||||
|
- name: Shared Types
|
||||||
|
description: |
|
||||||
|
Types provide an abstraction over the shared data.
|
||||||
|
Shared types can be edited concurrently by peers.
|
||||||
|
- Type
|
||||||
|
- YArray
|
||||||
|
- YMap
|
||||||
|
- YText
|
||||||
|
- YXmlElement
|
||||||
|
- YXmlFragment
|
||||||
|
- YXmlHook
|
||||||
|
- YXmlText
|
||||||
|
- name: Bindings
|
||||||
|
description: |
|
||||||
|
A binding handles data binding from a Yjs type to
|
||||||
|
a data object
|
||||||
|
- Binding
|
||||||
|
- DomBinding
|
||||||
|
- QuillBinding
|
||||||
|
- TextareaBinding
|
||||||
|
- name: Events
|
||||||
|
description: |
|
||||||
|
Events describe changes on shared types.
|
||||||
|
- YArrayEvent
|
||||||
|
- YEvent
|
||||||
|
- YMapEvent
|
||||||
|
- YTextEvent
|
||||||
|
- YXmlEvent
|
||||||
|
- name: Binary Encoding
|
||||||
|
description: |
|
||||||
|
Yjs efficiently encodes the Yjs model to a binary format.
|
||||||
|
This section describes utility functions for binary encoding and decoding.
|
||||||
|
- BinaryEncoder
|
||||||
|
- BinaryDecoder
|
||||||
|
- toBinary
|
||||||
|
- fromBinary
|
||||||
|
- name: Relative Position
|
||||||
|
- RelativePosition
|
||||||
|
- fromRelativePosition
|
||||||
|
- getRelativePosition
|
||||||
|
- name: Utility
|
1168
package-lock.json
generated
1168
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,9 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run lint",
|
"test": "npm run lint",
|
||||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||||
"lint": "standard",
|
"lint": "standard && documentation lint src/**",
|
||||||
|
"docs": "documentation build src/** -f html -o docs",
|
||||||
|
"serve-docs": "documentation serve src/**",
|
||||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||||
"postversion": "npm run dist",
|
"postversion": "npm run dist",
|
||||||
@ -64,7 +66,9 @@
|
|||||||
"rollup-regenerator-runtime": "^6.23.1",
|
"rollup-regenerator-runtime": "^6.23.1",
|
||||||
"rollup-watch": "^3.2.2",
|
"rollup-watch": "^3.2.2",
|
||||||
"standard": "^10.0.2",
|
"standard": "^10.0.2",
|
||||||
"tag-dist-files": "^0.1.6"
|
"tag-dist-files": "^0.1.6",
|
||||||
|
"esdoc": "^1.0.4",
|
||||||
|
"esdoc-standard-plugin": "^1.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^2.6.8"
|
"debug": "^2.6.8"
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import ID from '../Util/ID.js'
|
import ID from '../Util/ID.js'
|
||||||
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
|
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A BinaryDecoder handles the decoding of an ArrayBuffer
|
||||||
|
*/
|
||||||
export default class BinaryDecoder {
|
export default class BinaryDecoder {
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array|Buffer} buffer The binary data that this instance decodes
|
||||||
|
*/
|
||||||
constructor (buffer) {
|
constructor (buffer) {
|
||||||
if (buffer instanceof ArrayBuffer) {
|
if (buffer instanceof ArrayBuffer) {
|
||||||
this.uint8arr = new Uint8Array(buffer)
|
this.uint8arr = new Uint8Array(buffer)
|
||||||
@ -12,6 +18,7 @@ export default class BinaryDecoder {
|
|||||||
}
|
}
|
||||||
this.pos = 0
|
this.pos = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone this decoder instance
|
* Clone this decoder instance
|
||||||
* Optionally set a new position parameter
|
* Optionally set a new position parameter
|
||||||
@ -21,26 +28,32 @@ export default class BinaryDecoder {
|
|||||||
decoder.pos = newPos
|
decoder.pos = newPos
|
||||||
return decoder
|
return decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of bytes
|
* Number of bytes
|
||||||
*/
|
*/
|
||||||
get length () {
|
get length () {
|
||||||
return this.uint8arr.length
|
return this.uint8arr.length
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skip one byte, jump to the next position
|
* Skip one byte, jump to the next position
|
||||||
*/
|
*/
|
||||||
skip8 () {
|
skip8 () {
|
||||||
this.pos++
|
this.pos++
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read one byte as unsigned integer
|
* Read one byte as unsigned integer
|
||||||
*/
|
*/
|
||||||
readUint8 () {
|
readUint8 () {
|
||||||
return this.uint8arr[this.pos++]
|
return this.uint8arr[this.pos++]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read 4 bytes as unsigned integer
|
* Read 4 bytes as unsigned integer
|
||||||
|
*
|
||||||
|
* @return number An unsigned integer
|
||||||
*/
|
*/
|
||||||
readUint32 () {
|
readUint32 () {
|
||||||
let uint =
|
let uint =
|
||||||
@ -51,19 +64,24 @@ export default class BinaryDecoder {
|
|||||||
this.pos += 4
|
this.pos += 4
|
||||||
return uint
|
return uint
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look ahead without incrementing position
|
* Look ahead without incrementing position
|
||||||
* to the next byte and read it as unsigned integer
|
* to the next byte and read it as unsigned integer
|
||||||
|
*
|
||||||
|
* @return number An unsigned integer
|
||||||
*/
|
*/
|
||||||
peekUint8 () {
|
peekUint8 () {
|
||||||
return this.uint8arr[this.pos]
|
return this.uint8arr[this.pos]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read unsigned integer (32bit) with variable length
|
* Read unsigned integer (32bit) with variable length
|
||||||
* 1/8th of the storage is used as encoding overhead
|
* 1/8th of the storage is used as encoding overhead
|
||||||
* - numbers < 2^7 is stored in one byte
|
* * numbers < 2^7 is stored in one byte
|
||||||
* - numbers < 2^14 is stored in two bytes
|
* * numbers < 2^14 is stored in two bytes
|
||||||
* ..
|
*
|
||||||
|
* @return number An unsigned integer
|
||||||
*/
|
*/
|
||||||
readVarUint () {
|
readVarUint () {
|
||||||
let num = 0
|
let num = 0
|
||||||
@ -80,9 +98,12 @@ export default class BinaryDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read string of variable length
|
* Read string of variable length
|
||||||
* - varUint is used to store the length of the string
|
* * varUint is used to store the length of the string
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
readVarString () {
|
readVarString () {
|
||||||
let len = this.readVarUint()
|
let len = this.readVarUint()
|
||||||
@ -94,7 +115,7 @@ export default class BinaryDecoder {
|
|||||||
return decodeURIComponent(escape(encodedString))
|
return decodeURIComponent(escape(encodedString))
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Look ahead and read varString without incrementing position
|
* Look ahead and read varString without incrementing position
|
||||||
*/
|
*/
|
||||||
peekVarString () {
|
peekVarString () {
|
||||||
let pos = this.pos
|
let pos = this.pos
|
||||||
@ -104,8 +125,10 @@ export default class BinaryDecoder {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Read ID
|
* Read ID
|
||||||
* - If first varUint read is 0xFFFFFF a RootID is returned
|
* * If first varUint read is 0xFFFFFF a RootID is returned
|
||||||
* - Otherwise an ID is returned
|
* * Otherwise an ID is returned
|
||||||
|
*
|
||||||
|
* @return ID
|
||||||
*/
|
*/
|
||||||
readID () {
|
readID () {
|
||||||
let user = this.readVarUint()
|
let user = this.readVarUint()
|
||||||
|
@ -3,41 +3,81 @@ import { RootFakeUserID } from '../Util/RootID.js'
|
|||||||
const bits7 = 0b1111111
|
const bits7 = 0b1111111
|
||||||
const bits8 = 0b11111111
|
const bits8 = 0b11111111
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A BinaryEncoder handles the encoding to an ArrayBuffer
|
||||||
|
*/
|
||||||
export default class BinaryEncoder {
|
export default class BinaryEncoder {
|
||||||
constructor () {
|
constructor () {
|
||||||
// TODO: implement chained Uint8Array buffers instead of Array buffer
|
// TODO: implement chained Uint8Array buffers instead of Array buffer
|
||||||
this.data = []
|
this.data = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current length of the encoded data
|
||||||
|
*/
|
||||||
get length () {
|
get length () {
|
||||||
return this.data.length
|
return this.data.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current write pointer (the same as {@link length}).
|
||||||
|
*/
|
||||||
get pos () {
|
get pos () {
|
||||||
return this.data.length
|
return this.data.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an ArrayBuffer
|
||||||
|
*
|
||||||
|
* @return {Uint8Array}
|
||||||
|
*/
|
||||||
createBuffer () {
|
createBuffer () {
|
||||||
return Uint8Array.from(this.data).buffer
|
return Uint8Array.from(this.data).buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write one byte as an unsigned integer
|
||||||
|
*
|
||||||
|
* @param {number} num The number that is to be encoded
|
||||||
|
*/
|
||||||
writeUint8 (num) {
|
writeUint8 (num) {
|
||||||
this.data.push(num & bits8)
|
this.data.push(num & bits8)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write one byte as an unsigned Integer at a specific location
|
||||||
|
*
|
||||||
|
* @param {number} pos The location where the data will be written
|
||||||
|
* @param {number} num The number that is to
|
||||||
|
*/
|
||||||
setUint8 (pos, num) {
|
setUint8 (pos, num) {
|
||||||
this.data[pos] = num & bits8
|
this.data[pos] = num & bits8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write two bytes as an unsigned integer
|
||||||
|
*
|
||||||
|
* @param {number} pos The number that is to be encoded
|
||||||
|
*/
|
||||||
writeUint16 (num) {
|
writeUint16 (num) {
|
||||||
this.data.push(num & bits8, (num >>> 8) & bits8)
|
this.data.push(num & bits8, (num >>> 8) & bits8)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Write two bytes as an unsigned integer at a specific location
|
||||||
|
*
|
||||||
|
* @param {number} pos The location where the data will be written
|
||||||
|
* @param {number} num The number that is to
|
||||||
|
*/
|
||||||
setUint16 (pos, num) {
|
setUint16 (pos, num) {
|
||||||
this.data[pos] = num & bits8
|
this.data[pos] = num & bits8
|
||||||
this.data[pos + 1] = (num >>> 8) & bits8
|
this.data[pos + 1] = (num >>> 8) & bits8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write two bytes as an unsigned integer
|
||||||
|
*
|
||||||
|
* @param {number} pos The number that is to be encoded
|
||||||
|
*/
|
||||||
writeUint32 (num) {
|
writeUint32 (num) {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
this.data.push(num & bits8)
|
this.data.push(num & bits8)
|
||||||
@ -45,6 +85,12 @@ export default class BinaryEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write two bytes as an unsigned integer at a specific location
|
||||||
|
*
|
||||||
|
* @param {number} pos The location where the data will be written
|
||||||
|
* @param {number} num The number that is to
|
||||||
|
*/
|
||||||
setUint32 (pos, num) {
|
setUint32 (pos, num) {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
this.data[pos + i] = num & bits8
|
this.data[pos + i] = num & bits8
|
||||||
@ -52,6 +98,11 @@ export default class BinaryEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a variable length unsigned integer
|
||||||
|
*
|
||||||
|
* @param {number} pos The number that is to be encoded
|
||||||
|
*/
|
||||||
writeVarUint (num) {
|
writeVarUint (num) {
|
||||||
while (num >= 0b10000000) {
|
while (num >= 0b10000000) {
|
||||||
this.data.push(0b10000000 | (bits7 & num))
|
this.data.push(0b10000000 | (bits7 & num))
|
||||||
@ -60,6 +111,11 @@ export default class BinaryEncoder {
|
|||||||
this.data.push(bits7 & num)
|
this.data.push(bits7 & num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a variable length string.
|
||||||
|
*
|
||||||
|
* @param {number} pos The number that is to be encoded
|
||||||
|
*/
|
||||||
writeVarString (str) {
|
writeVarString (str) {
|
||||||
let encodedString = unescape(encodeURIComponent(str))
|
let encodedString = unescape(encodeURIComponent(str))
|
||||||
let bytes = encodedString.split('').map(c => c.codePointAt())
|
let bytes = encodedString.split('').map(c => c.codePointAt())
|
||||||
@ -70,6 +126,11 @@ export default class BinaryEncoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write an ID at the current position
|
||||||
|
*
|
||||||
|
* @param {ID} id
|
||||||
|
*/
|
||||||
writeID (id) {
|
writeID (id) {
|
||||||
const user = id.user
|
const user = id.user
|
||||||
this.writeVarUint(user)
|
this.writeVarUint(user)
|
||||||
|
@ -1,12 +1,34 @@
|
|||||||
|
|
||||||
import { createMutualExclude } from '../Util/mutualExclude.js'
|
import { createMutualExclude } from '../Util/mutualExclude.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for bindings
|
||||||
|
*
|
||||||
|
* A binding handles data binding from a Yjs type to a data object. For example,
|
||||||
|
* you can bind a Quill editor instance to a YText instance with the `QuillBinding` class.
|
||||||
|
*
|
||||||
|
* It is expected that a concrete implementation accepts two parameters
|
||||||
|
* (type and binding target).
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const quill = new Quill(document.createElement('div'))
|
||||||
|
* const type = y.define('quill', Y.Text)
|
||||||
|
* const binding = new Y.QuillBinding(quill, type)
|
||||||
|
*
|
||||||
|
*/
|
||||||
export default class Binding {
|
export default class Binding {
|
||||||
|
/**
|
||||||
|
* @param {YType} type Yjs type
|
||||||
|
* @param {any} target Binding Target
|
||||||
|
*/
|
||||||
constructor (type, target) {
|
constructor (type, target) {
|
||||||
this.type = type
|
this.type = type
|
||||||
this.target = target
|
this.target = target
|
||||||
this._mutualExclude = createMutualExclude()
|
this._mutualExclude = createMutualExclude()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Remove all data observers (both from the type and th target).
|
||||||
|
*/
|
||||||
destroy () {
|
destroy () {
|
||||||
this.type = null
|
this.type = null
|
||||||
this.target = null
|
this.target = null
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import Binding from './Binding.js'
|
import Binding from './Binding.js'
|
||||||
|
|
||||||
function typeObserver (event) {
|
function typeObserver (event) {
|
||||||
@ -16,17 +15,29 @@ function quillObserver (delta) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Binding that binds a YText type to a Quill editor
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const quill = new Quill(document.createElement('div'))
|
||||||
|
* const type = y.define('quill', Y.Text)
|
||||||
|
* const binding = new Y.QuillBinding(quill, type)
|
||||||
|
*/
|
||||||
export default class QuillBinding extends Binding {
|
export default class QuillBinding extends Binding {
|
||||||
constructor (textType, quillInstance) {
|
/**
|
||||||
// Binding handles textType as this.type and quillInstance as this.target
|
* @param {YText} textType
|
||||||
super(textType, quillInstance)
|
* @param {Quill} quill
|
||||||
|
*/
|
||||||
|
constructor (textType, quill) {
|
||||||
|
// Binding handles textType as this.type and quill as this.target
|
||||||
|
super(textType, quill)
|
||||||
// set initial value
|
// set initial value
|
||||||
quillInstance.setContents(textType.toDelta(), 'yjs')
|
quill.setContents(textType.toDelta(), 'yjs')
|
||||||
// Observers are handled by this class
|
// Observers are handled by this class
|
||||||
this._typeObserver = typeObserver.bind(this)
|
this._typeObserver = typeObserver.bind(this)
|
||||||
this._quillObserver = quillObserver.bind(this)
|
this._quillObserver = quillObserver.bind(this)
|
||||||
textType.observe(this._typeObserver)
|
textType.observe(this._typeObserver)
|
||||||
quillInstance.on('text-change', this._quillObserver)
|
quill.on('text-change', this._quillObserver)
|
||||||
}
|
}
|
||||||
destroy () {
|
destroy () {
|
||||||
// Remove everything that is handled by this class
|
// Remove everything that is handled by this class
|
||||||
|
@ -24,6 +24,17 @@ function domObserver () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A binding that binds a YText to a dom textarea.
|
||||||
|
*
|
||||||
|
* This binding will automatically be destroyed when it's parent is deleted
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const textare = document.createElement('textarea')
|
||||||
|
* const type = y.define('textarea', Y.Text)
|
||||||
|
* const binding = new Y.QuillBinding(textarea, type)
|
||||||
|
*
|
||||||
|
*/
|
||||||
export default class TextareaBinding extends Binding {
|
export default class TextareaBinding extends Binding {
|
||||||
constructor (textType, domTextarea) {
|
constructor (textType, domTextarea) {
|
||||||
// Binding handles textType as this.type and domTextarea as this.target
|
// Binding handles textType as this.type and domTextarea as this.target
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
|
|
||||||
import { writeStructs } from './syncStep1.js'
|
import { writeStructs } from './syncStep1.js'
|
||||||
import { integrateRemoteStructs } from './integrateRemoteStructs.js'
|
import { integrateRemoteStructs } from './integrateRemoteStructs.js'
|
||||||
import { readDeleteSet, writeDeleteSet } from './deleteSet.js'
|
import { readDeleteSet, writeDeleteSet } from './deleteSet.js'
|
||||||
import BinaryEncoder from '../Binary/Encoder.js'
|
import BinaryEncoder from '../Binary/Encoder.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the Decoder and fill the Yjs instance with data in the decoder.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance
|
||||||
|
* @param {BinaryDecoder} decoder The BinaryDecoder to read from.
|
||||||
|
*/
|
||||||
export function fromBinary (y, decoder) {
|
export function fromBinary (y, decoder) {
|
||||||
y.transact(function () {
|
y.transact(function () {
|
||||||
integrateRemoteStructs(y, decoder)
|
integrateRemoteStructs(y, decoder)
|
||||||
@ -10,6 +17,13 @@ export function fromBinary (y, decoder) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the Yjs model to binary format.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance
|
||||||
|
* @return {BinaryEncoder} The encoder instance that can be transformed
|
||||||
|
* to ArrayBuffer or Buffer.
|
||||||
|
*/
|
||||||
export function toBinary (y) {
|
export function toBinary (y) {
|
||||||
let encoder = new BinaryEncoder()
|
let encoder = new BinaryEncoder()
|
||||||
writeStructs(y, encoder, new Map())
|
writeStructs(y, encoder, new Map())
|
||||||
|
@ -11,6 +11,7 @@ class MissingEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @private
|
||||||
* Integrate remote struct
|
* Integrate remote struct
|
||||||
* When a remote struct is integrated, other structs might be ready to ready to
|
* When a remote struct is integrated, other structs might be ready to ready to
|
||||||
* integrate.
|
* integrate.
|
||||||
|
@ -30,6 +30,11 @@ export function sendSyncStep1 (connector, syncUser) {
|
|||||||
connector.send(syncUser, encoder.createBuffer())
|
connector.send(syncUser, encoder.createBuffer())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Write all Items that are not not included in ss to
|
||||||
|
* the encoder object.
|
||||||
|
*/
|
||||||
export function writeStructs (y, encoder, ss) {
|
export function writeStructs (y, encoder, ss) {
|
||||||
const lenPos = encoder.pos
|
const lenPos = encoder.pos
|
||||||
encoder.writeUint32(0)
|
encoder.writeUint32(0)
|
||||||
|
@ -13,6 +13,10 @@ function getFreshCnf () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Abstract persistence class.
|
||||||
|
*/
|
||||||
export default class AbstractPersistence {
|
export default class AbstractPersistence {
|
||||||
constructor (opts) {
|
constructor (opts) {
|
||||||
this.opts = opts
|
this.opts = opts
|
||||||
|
@ -3,6 +3,7 @@ import ID from '../Util/ID.js'
|
|||||||
import { logID } from '../MessageHandler/messageToString.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @private
|
||||||
* Delete all items in an ID-range
|
* Delete all items in an ID-range
|
||||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
||||||
*/
|
*/
|
||||||
@ -35,13 +36,26 @@ export function deleteItemRange (y, user, clock, range) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete is not a real struct. It will not be saved in OS
|
* @private
|
||||||
|
* A Delete change is not a real Item, but it provides the same interface as an
|
||||||
|
* Item. The only difference is that it will not be saved in the ItemStore
|
||||||
|
* (OperationStore), but instead it is safed in the DeleteStore.
|
||||||
*/
|
*/
|
||||||
export default class Delete {
|
export default class Delete {
|
||||||
constructor () {
|
constructor () {
|
||||||
this._target = null
|
this._target = null
|
||||||
this._length = null
|
this._length = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Read the next Item in a Decoder and fill this Item with the read data.
|
||||||
|
*
|
||||||
|
* This is called when data is received from a remote peer.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||||
|
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||||
|
*/
|
||||||
_fromBinary (y, decoder) {
|
_fromBinary (y, decoder) {
|
||||||
// TODO: set target, and add it to missing if not found
|
// TODO: set target, and add it to missing if not found
|
||||||
// There is an edge case in p2p networks!
|
// There is an edge case in p2p networks!
|
||||||
@ -54,15 +68,32 @@ export default class Delete {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||||
|
*/
|
||||||
_toBinary (encoder) {
|
_toBinary (encoder) {
|
||||||
encoder.writeUint8(getReference(this.constructor))
|
encoder.writeUint8(getReference(this.constructor))
|
||||||
encoder.writeID(this._targetID)
|
encoder.writeID(this._targetID)
|
||||||
encoder.writeVarUint(this._length)
|
encoder.writeVarUint(this._length)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - If created remotely (a remote user deleted something),
|
* @private
|
||||||
|
* Integrates this Item into the shared structure.
|
||||||
|
*
|
||||||
|
* This method actually applies the change to the Yjs instance. In the case of
|
||||||
|
* Delete it marks the delete target as deleted.
|
||||||
|
*
|
||||||
|
* * If created remotely (a remote user deleted something),
|
||||||
* this Delete is applied to all structs in id-range.
|
* this Delete is applied to all structs in id-range.
|
||||||
* - If created lokally (e.g. when y-array deletes a range of elements),
|
* * If created lokally (e.g. when y-array deletes a range of elements),
|
||||||
* this struct is broadcasted only (it is already executed)
|
* this struct is broadcasted only (it is already executed)
|
||||||
*/
|
*/
|
||||||
_integrate (y, locallyCreated = false) {
|
_integrate (y, locallyCreated = false) {
|
||||||
@ -78,6 +109,12 @@ export default class Delete {
|
|||||||
y.persistence.saveStruct(y, this)
|
y.persistence.saveStruct(y, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Transform this Delete to a readable format.
|
||||||
|
* Useful for logging as all Items implement this method.
|
||||||
|
*/
|
||||||
_logString () {
|
_logString () {
|
||||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,12 @@ import Delete from './Delete.js'
|
|||||||
import { transactionTypeChanged } from '../Transaction.js'
|
import { transactionTypeChanged } from '../Transaction.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper utility to split an Item (see _splitAt)
|
* @private
|
||||||
* - copy all properties from a to b
|
* Helper utility to split an Item (see {@link Item#_splitAt})
|
||||||
* - connect a to b
|
* - copies all properties from a to b
|
||||||
|
* - connects a to b
|
||||||
* - assigns the correct _id
|
* - assigns the correct _id
|
||||||
* - save b to os
|
* - saves b to os
|
||||||
*/
|
*/
|
||||||
export function splitHelper (y, a, b, diff) {
|
export function splitHelper (y, a, b, diff) {
|
||||||
const aID = a._id
|
const aID = a._id
|
||||||
@ -46,6 +47,10 @@ export function splitHelper (y, a, b, diff) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Abstract class that represents any content.
|
||||||
|
*/
|
||||||
export default class Item {
|
export default class Item {
|
||||||
constructor () {
|
constructor () {
|
||||||
this._id = null
|
this._id = null
|
||||||
@ -58,14 +63,18 @@ export default class Item {
|
|||||||
this._deleted = false
|
this._deleted = false
|
||||||
this._redone = null
|
this._redone = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a operation with the same effect (without position effect)
|
* @private
|
||||||
|
* Creates an Item with the same effect as this Item (without position effect)
|
||||||
*/
|
*/
|
||||||
_copy () {
|
_copy () {
|
||||||
return new this.constructor()
|
return new this.constructor()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redo the effect of this operation.
|
* @private
|
||||||
|
* Redoes the effect of this operation.
|
||||||
*/
|
*/
|
||||||
_redo (y) {
|
_redo (y) {
|
||||||
if (this._redone !== null) {
|
if (this._redone !== null) {
|
||||||
@ -106,27 +115,43 @@ export default class Item {
|
|||||||
return struct
|
return struct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Computes the last content address of this Item.
|
||||||
|
*/
|
||||||
get _lastId () {
|
get _lastId () {
|
||||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
return new ID(this._id.user, this._id.clock + this._length - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Computes the length of this Item.
|
||||||
|
*/
|
||||||
get _length () {
|
get _length () {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some elements are not supposed to be addressable. For example, an
|
* @private
|
||||||
* ItemFormat should not be retrievable via yarray.get(pos)
|
* 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
|
||||||
*/
|
*/
|
||||||
get _countable () {
|
get _countable () {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits this struct so that another struct can be inserted in-between.
|
* @private
|
||||||
|
* Splits this Item so that another Items can be inserted in-between.
|
||||||
* This must be overwritten if _length > 1
|
* This must be overwritten if _length > 1
|
||||||
* Returns right part after split
|
* Returns right part after split
|
||||||
* - diff === 0 => this
|
* * diff === 0 => this
|
||||||
* - diff === length => this._right
|
* * diff === length => this._right
|
||||||
* - otherwise => split _content and return right part of split
|
* * otherwise => split _content and return right part of split
|
||||||
* (see ItemJSON/ItemString for implementation)
|
* (see {@link ItemJSON}/{@link ItemString} for implementation)
|
||||||
*/
|
*/
|
||||||
_splitAt (y, diff) {
|
_splitAt (y, diff) {
|
||||||
if (diff === 0) {
|
if (diff === 0) {
|
||||||
@ -134,6 +159,15 @@ export default class Item {
|
|||||||
}
|
}
|
||||||
return this._right
|
return this._right
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Mark this Item as deleted.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance
|
||||||
|
* @param {boolean} createDelete Whether to propagate a message that this
|
||||||
|
* Type was deleted.
|
||||||
|
*/
|
||||||
_delete (y, createDelete = true) {
|
_delete (y, createDelete = true) {
|
||||||
if (!this._deleted) {
|
if (!this._deleted) {
|
||||||
this._deleted = true
|
this._deleted = true
|
||||||
@ -152,17 +186,27 @@ export default class Item {
|
|||||||
y._transaction.deletedStructs.add(this)
|
y._transaction.deletedStructs.add(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is called right before this struct receives any children.
|
* @private
|
||||||
|
* This is called right before this Item receives any children.
|
||||||
* It can be overwritten to apply pending changes before applying remote changes
|
* It can be overwritten to apply pending changes before applying remote changes
|
||||||
*/
|
*/
|
||||||
_beforeChange () {
|
_beforeChange () {
|
||||||
// nop
|
// nop
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
* - Integrate the struct so that other types/structs can see it
|
/**
|
||||||
* - Add this struct to y.os
|
* @private
|
||||||
* - Check if this is struct deleted
|
* Integrates this Item into the shared structure.
|
||||||
|
*
|
||||||
|
* This method actually applies the change to the Yjs instance. In case of
|
||||||
|
* Item it connects _left and _right to this Item and calls the
|
||||||
|
* {@link Item#beforeChange} method.
|
||||||
|
*
|
||||||
|
* * Integrate the struct so that other types/structs can see it
|
||||||
|
* * Add this struct to y.os
|
||||||
|
* * Check if this is struct deleted
|
||||||
*/
|
*/
|
||||||
_integrate (y) {
|
_integrate (y) {
|
||||||
y._transaction.newTypes.add(this)
|
y._transaction.newTypes.add(this)
|
||||||
@ -188,6 +232,7 @@ export default class Item {
|
|||||||
// or this types is new
|
// or this types is new
|
||||||
this._parent._beforeChange()
|
this._parent._beforeChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
# $this has to find a unique position between origin and the next known character
|
# $this has to find a unique position between origin and the next known character
|
||||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||||
@ -280,6 +325,16 @@ export default class Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||||
|
*/
|
||||||
_toBinary (encoder) {
|
_toBinary (encoder) {
|
||||||
encoder.writeUint8(getReference(this.constructor))
|
encoder.writeUint8(getReference(this.constructor))
|
||||||
let info = 0
|
let info = 0
|
||||||
@ -320,6 +375,16 @@ export default class Item {
|
|||||||
encoder.writeVarString(JSON.stringify(this._parentSub))
|
encoder.writeVarString(JSON.stringify(this._parentSub))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Read the next Item in a Decoder and fill this Item with the read data.
|
||||||
|
*
|
||||||
|
* This is called when data is received from a remote peer.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||||
|
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||||
|
*/
|
||||||
_fromBinary (y, decoder) {
|
_fromBinary (y, decoder) {
|
||||||
let missing = []
|
let missing = []
|
||||||
const info = decoder.readUint8()
|
const info = decoder.readUint8()
|
||||||
|
@ -30,6 +30,9 @@ export function getListItemIDByPosition (type, i) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract Yjs Type class
|
||||||
|
*/
|
||||||
export default class Type extends Item {
|
export default class Type extends Item {
|
||||||
constructor () {
|
constructor () {
|
||||||
super()
|
super()
|
||||||
@ -39,6 +42,20 @@ export default class Type extends Item {
|
|||||||
this._eventHandler = new EventHandler()
|
this._eventHandler = new EventHandler()
|
||||||
this._deepEventHandler = new EventHandler()
|
this._deepEventHandler = new EventHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the path from this type to the specified target.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* It should be accessible via `this.get(result[0]).get(result[1])..``
|
||||||
|
* const path = type.getPathTo(child)
|
||||||
|
* // assuming `type instanceof YArray`
|
||||||
|
* console.log(path) // might look like => [2, 'key1']
|
||||||
|
* child === type.get(path[0]).get(path[1])
|
||||||
|
*
|
||||||
|
* @param {YType} type Type target
|
||||||
|
* @return {Array<string>} Path to the target
|
||||||
|
*/
|
||||||
getPathTo (type) {
|
getPathTo (type) {
|
||||||
if (type === this) {
|
if (type === this) {
|
||||||
return []
|
return []
|
||||||
@ -65,6 +82,12 @@ export default class Type extends Item {
|
|||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Call event listeners with an event. This will also add an event to all
|
||||||
|
* parents (for `.observeDeep` handlers).
|
||||||
|
*/
|
||||||
_callEventHandler (transaction, event) {
|
_callEventHandler (transaction, event) {
|
||||||
const changedParentTypes = transaction.changedParentTypes
|
const changedParentTypes = transaction.changedParentTypes
|
||||||
this._eventHandler.callEventListeners(transaction, event)
|
this._eventHandler.callEventListeners(transaction, event)
|
||||||
@ -79,6 +102,14 @@ export default class Type extends Item {
|
|||||||
type = type._parent
|
type = type._parent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Helper method to transact if the y instance is available.
|
||||||
|
*
|
||||||
|
* TODO: Currently event handlers are not thrown when a type is not registered
|
||||||
|
* with a Yjs instance.
|
||||||
|
*/
|
||||||
_transact (f) {
|
_transact (f) {
|
||||||
const y = this._y
|
const y = this._y
|
||||||
if (y !== null) {
|
if (y !== null) {
|
||||||
@ -87,18 +118,53 @@ export default class Type extends Item {
|
|||||||
f(y)
|
f(y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe all events that are created on this type.
|
||||||
|
*
|
||||||
|
* @param {Function} f Observer function
|
||||||
|
*/
|
||||||
observe (f) {
|
observe (f) {
|
||||||
this._eventHandler.addEventListener(f)
|
this._eventHandler.addEventListener(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe all events that are created by this type and its children.
|
||||||
|
*
|
||||||
|
* @param {Function} f Observer function
|
||||||
|
*/
|
||||||
observeDeep (f) {
|
observeDeep (f) {
|
||||||
this._deepEventHandler.addEventListener(f)
|
this._deepEventHandler.addEventListener(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister an observer function.
|
||||||
|
*
|
||||||
|
* @param {Function} f Observer function
|
||||||
|
*/
|
||||||
unobserve (f) {
|
unobserve (f) {
|
||||||
this._eventHandler.removeEventListener(f)
|
this._eventHandler.removeEventListener(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister an observer function.
|
||||||
|
*
|
||||||
|
* @param {Function} f Observer function
|
||||||
|
*/
|
||||||
unobserveDeep (f) {
|
unobserveDeep (f) {
|
||||||
this._deepEventHandler.removeEventListener(f)
|
this._deepEventHandler.removeEventListener(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* 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 {Y} y The Yjs instance
|
||||||
|
*/
|
||||||
_integrate (y) {
|
_integrate (y) {
|
||||||
super._integrate(y)
|
super._integrate(y)
|
||||||
this._y = y
|
this._y = y
|
||||||
@ -117,6 +183,15 @@ export default class Type extends Item {
|
|||||||
integrateChildren(y, t)
|
integrateChildren(y, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Mark this Item as deleted.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance
|
||||||
|
* @param {boolean} createDelete Whether to propagate a message that this
|
||||||
|
* Type was deleted.
|
||||||
|
*/
|
||||||
_delete (y, createDelete) {
|
_delete (y, createDelete) {
|
||||||
super._delete(y, createDelete)
|
super._delete(y, createDelete)
|
||||||
y._transaction.changedTypes.delete(this)
|
y._transaction.changedTypes.delete(this)
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Changes that are created within a transaction are bundled and sent as one
|
||||||
|
* message to the remote peers. This implies that the changes are applied
|
||||||
|
* in one flush and at most one {@link YEvent} per type is created.
|
||||||
|
*
|
||||||
|
* It is best to bundle as many changes in a single Transaction as possible.
|
||||||
|
* This way only few changes need to be computed
|
||||||
|
*/
|
||||||
export default class Transaction {
|
export default class Transaction {
|
||||||
constructor (y) {
|
constructor (y) {
|
||||||
this.y = y
|
this.y = y
|
||||||
|
@ -4,6 +4,13 @@ import ItemString from '../Struct/ItemString.js'
|
|||||||
import { logID } from '../MessageHandler/messageToString.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
import YEvent from '../Util/YEvent.js'
|
import YEvent from '../Util/YEvent.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event that describes the changes on a YArray
|
||||||
|
*
|
||||||
|
* @param {YArray} yarray The changed type
|
||||||
|
* @param {Boolean} remote Whether the changed was caused by a remote peer
|
||||||
|
* @param {Transaction} transaction The transaction object
|
||||||
|
*/
|
||||||
export class YArrayEvent extends YEvent {
|
export class YArrayEvent extends YEvent {
|
||||||
constructor (yarray, remote, transaction) {
|
constructor (yarray, remote, transaction) {
|
||||||
super(yarray)
|
super(yarray)
|
||||||
@ -12,6 +19,12 @@ export class YArrayEvent extends YEvent {
|
|||||||
this._addedElements = null
|
this._addedElements = null
|
||||||
this._removedElements = null
|
this._removedElements = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Child elements that were added in this transaction.
|
||||||
|
*
|
||||||
|
* @return {Set}
|
||||||
|
*/
|
||||||
get addedElements () {
|
get addedElements () {
|
||||||
if (this._addedElements === null) {
|
if (this._addedElements === null) {
|
||||||
const target = this.target
|
const target = this.target
|
||||||
@ -26,6 +39,12 @@ export class YArrayEvent extends YEvent {
|
|||||||
}
|
}
|
||||||
return this._addedElements
|
return this._addedElements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Child elements that were removed in this transaction.
|
||||||
|
*
|
||||||
|
* @return {Set}
|
||||||
|
*/
|
||||||
get removedElements () {
|
get removedElements () {
|
||||||
if (this._removedElements === null) {
|
if (this._removedElements === null) {
|
||||||
const target = this.target
|
const target = this.target
|
||||||
@ -42,29 +61,54 @@ export class YArrayEvent extends YEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A shared Array implementation.
|
||||||
|
*/
|
||||||
export default class YArray extends Type {
|
export default class YArray extends Type {
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Creates YArray Event and calls observers.
|
||||||
|
*/
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
_callObserver (transaction, parentSubs, remote) {
|
||||||
this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction))
|
this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction))
|
||||||
}
|
}
|
||||||
get (pos) {
|
|
||||||
|
/**
|
||||||
|
* Returns the i-th element from a YArray.
|
||||||
|
*
|
||||||
|
* @param {Integer} index The index of the element to return from the YArray
|
||||||
|
*/
|
||||||
|
get (index) {
|
||||||
let n = this._start
|
let n = this._start
|
||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
if (!n._deleted && n._countable) {
|
if (!n._deleted && n._countable) {
|
||||||
if (pos < n._length) {
|
if (index < n._length) {
|
||||||
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
||||||
return n._content[pos]
|
return n._content[index]
|
||||||
} else {
|
} else {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos -= n._length
|
index -= n._length
|
||||||
}
|
}
|
||||||
n = n._right
|
n = n._right
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this YArray to a JavaScript Array.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
toArray () {
|
toArray () {
|
||||||
return this.map(c => c)
|
return this.map(c => c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this Shared Type to a JSON object.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
toJSON () {
|
toJSON () {
|
||||||
return this.map(c => {
|
return this.map(c => {
|
||||||
if (c instanceof Type) {
|
if (c instanceof Type) {
|
||||||
@ -77,6 +121,15 @@ export default class YArray extends Type {
|
|||||||
return c
|
return c
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an Array with the result of calling a provided function on every
|
||||||
|
* element of this YArray.
|
||||||
|
*
|
||||||
|
* @param {Function} f Function that produces an element of the new Array
|
||||||
|
* @return {Array} A new array with each element being the result of the
|
||||||
|
* callback function
|
||||||
|
*/
|
||||||
map (f) {
|
map (f) {
|
||||||
const res = []
|
const res = []
|
||||||
this.forEach((c, i) => {
|
this.forEach((c, i) => {
|
||||||
@ -84,25 +137,35 @@ export default class YArray extends Type {
|
|||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a provided function on once on overy element of this YArray.
|
||||||
|
*
|
||||||
|
* @param {Function} f A function to execute on every element of this YArray.
|
||||||
|
*/
|
||||||
forEach (f) {
|
forEach (f) {
|
||||||
let pos = 0
|
let index = 0
|
||||||
let n = this._start
|
let n = this._start
|
||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
if (!n._deleted && n._countable) {
|
if (!n._deleted && n._countable) {
|
||||||
if (n instanceof Type) {
|
if (n instanceof Type) {
|
||||||
f(n, pos++, this)
|
f(n, index++, this)
|
||||||
} else {
|
} else {
|
||||||
const content = n._content
|
const content = n._content
|
||||||
const contentLen = content.length
|
const contentLen = content.length
|
||||||
for (let i = 0; i < contentLen; i++) {
|
for (let i = 0; i < contentLen; i++) {
|
||||||
pos++
|
index++
|
||||||
f(content[i], pos, this)
|
f(content[i], index, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
n = n._right
|
n = n._right
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the length of this YArray.
|
||||||
|
*/
|
||||||
get length () {
|
get length () {
|
||||||
let length = 0
|
let length = 0
|
||||||
let n = this._start
|
let n = this._start
|
||||||
@ -114,6 +177,7 @@ export default class YArray extends Type {
|
|||||||
}
|
}
|
||||||
return length
|
return length
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator] () {
|
[Symbol.iterator] () {
|
||||||
return {
|
return {
|
||||||
next: function () {
|
next: function () {
|
||||||
@ -143,14 +207,21 @@ export default class YArray extends Type {
|
|||||||
_count: 0
|
_count: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete (pos, length = 1) {
|
|
||||||
|
/**
|
||||||
|
* Deletes elements starting from an index.
|
||||||
|
*
|
||||||
|
* @param {Integer} index Index at which to start deleting elements
|
||||||
|
* @param {Integer} length The number of elements to remove. Defaults to 1.
|
||||||
|
*/
|
||||||
|
delete (index, length = 1) {
|
||||||
this._y.transact(() => {
|
this._y.transact(() => {
|
||||||
let item = this._start
|
let item = this._start
|
||||||
let count = 0
|
let count = 0
|
||||||
while (item !== null && length > 0) {
|
while (item !== null && length > 0) {
|
||||||
if (!item._deleted && item._countable) {
|
if (!item._deleted && item._countable) {
|
||||||
if (count <= pos && pos < count + item._length) {
|
if (count <= index && index < count + item._length) {
|
||||||
const diffDel = pos - count
|
const diffDel = index - count
|
||||||
item = item._splitAt(this._y, diffDel)
|
item = item._splitAt(this._y, diffDel)
|
||||||
item._splitAt(this._y, length)
|
item._splitAt(this._y, length)
|
||||||
length -= item._length
|
length -= item._length
|
||||||
@ -167,6 +238,14 @@ export default class YArray extends Type {
|
|||||||
throw new Error('Delete exceeds the range of the YArray')
|
throw new Error('Delete exceeds the range of the YArray')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Inserts content after an element container.
|
||||||
|
*
|
||||||
|
* @param {Item} left The element container to use as a reference.
|
||||||
|
* @param {Array} content The Array of content to insert (see {@see insert})
|
||||||
|
*/
|
||||||
insertAfter (left, content) {
|
insertAfter (left, content) {
|
||||||
this._transact(y => {
|
this._transact(y => {
|
||||||
let right
|
let right
|
||||||
@ -224,7 +303,24 @@ export default class YArray extends Type {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
insert (pos, content) {
|
|
||||||
|
/**
|
||||||
|
* 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(2, [1, 2])
|
||||||
|
*
|
||||||
|
* @param {Integer} index The index to insert content at.
|
||||||
|
* @param {Array} content The array of content
|
||||||
|
*/
|
||||||
|
insert (index, content) {
|
||||||
this._transact(() => {
|
this._transact(() => {
|
||||||
let left = null
|
let left = null
|
||||||
let right = this._start
|
let right = this._start
|
||||||
@ -232,8 +328,8 @@ export default class YArray extends Type {
|
|||||||
const y = this._y
|
const y = this._y
|
||||||
while (right !== null) {
|
while (right !== null) {
|
||||||
const rightLen = right._deleted ? 0 : (right._length - 1)
|
const rightLen = right._deleted ? 0 : (right._length - 1)
|
||||||
if (count <= pos && pos <= count + rightLen) {
|
if (count <= index && index <= count + rightLen) {
|
||||||
const splitDiff = pos - count
|
const splitDiff = index - count
|
||||||
right = right._splitAt(y, splitDiff)
|
right = right._splitAt(y, splitDiff)
|
||||||
left = right._left
|
left = right._left
|
||||||
count += splitDiff
|
count += splitDiff
|
||||||
@ -245,12 +341,18 @@ export default class YArray extends Type {
|
|||||||
left = right
|
left = right
|
||||||
right = right._right
|
right = right._right
|
||||||
}
|
}
|
||||||
if (pos > count) {
|
if (index > count) {
|
||||||
throw new Error('Position exceeds array range!')
|
throw new Error('Index exceeds array range!')
|
||||||
}
|
}
|
||||||
this.insertAfter(left, content)
|
this.insertAfter(left, content)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends content to this YArray.
|
||||||
|
*
|
||||||
|
* @param {Array} content Array of content to append.
|
||||||
|
*/
|
||||||
push (content) {
|
push (content) {
|
||||||
let n = this._start
|
let n = this._start
|
||||||
let lastUndeleted = null
|
let lastUndeleted = null
|
||||||
@ -262,6 +364,12 @@ export default class YArray extends Type {
|
|||||||
}
|
}
|
||||||
this.insertAfter(lastUndeleted, content)
|
this.insertAfter(lastUndeleted, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Transform this YArray to a readable format.
|
||||||
|
* Useful for logging as all Items implement this method.
|
||||||
|
*/
|
||||||
_logString () {
|
_logString () {
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
const left = this._left !== null ? this._left._lastId : null
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
const origin = this._origin !== null ? this._origin._lastId : null
|
||||||
|
@ -4,7 +4,14 @@ import ItemJSON from '../Struct/ItemJSON.js'
|
|||||||
import { logID } from '../MessageHandler/messageToString.js'
|
import { logID } from '../MessageHandler/messageToString.js'
|
||||||
import YEvent from '../Util/YEvent.js'
|
import YEvent from '../Util/YEvent.js'
|
||||||
|
|
||||||
class YMapEvent extends YEvent {
|
/**
|
||||||
|
* Event that describes the changes on a YMap.
|
||||||
|
*
|
||||||
|
* @param {YMap} ymap The YArray that changed.
|
||||||
|
* @param {Set<any>} subs The keys that changed.
|
||||||
|
* @param {boolean} remote Whether the change was created by a remote peer.
|
||||||
|
*/
|
||||||
|
export class YMapEvent extends YEvent {
|
||||||
constructor (ymap, subs, remote) {
|
constructor (ymap, subs, remote) {
|
||||||
super(ymap)
|
super(ymap)
|
||||||
this.keysChanged = subs
|
this.keysChanged = subs
|
||||||
@ -12,10 +19,23 @@ class YMapEvent extends YEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A shared Map implementation.
|
||||||
|
*/
|
||||||
export default class YMap extends Type {
|
export default class YMap extends Type {
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Creates YMap Event and calls observers.
|
||||||
|
*/
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
_callObserver (transaction, parentSubs, remote) {
|
||||||
this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote))
|
this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this Shared Type to a JSON object.
|
||||||
|
*
|
||||||
|
* @return {Object}
|
||||||
|
*/
|
||||||
toJSON () {
|
toJSON () {
|
||||||
const map = {}
|
const map = {}
|
||||||
for (let [key, item] of this._map) {
|
for (let [key, item] of this._map) {
|
||||||
@ -35,7 +55,14 @@ export default class YMap extends Type {
|
|||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the keys for each element in the YMap Type.
|
||||||
|
*
|
||||||
|
* @return {Array}
|
||||||
|
*/
|
||||||
keys () {
|
keys () {
|
||||||
|
// TODO: Should return either Iterator or Set!
|
||||||
let keys = []
|
let keys = []
|
||||||
for (let [key, value] of this._map) {
|
for (let [key, value] of this._map) {
|
||||||
if (!value._deleted) {
|
if (!value._deleted) {
|
||||||
@ -44,6 +71,12 @@ export default class YMap extends Type {
|
|||||||
}
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a specified element from this YMap.
|
||||||
|
*
|
||||||
|
* @param {encodable} key The key of the element to remove.
|
||||||
|
*/
|
||||||
delete (key) {
|
delete (key) {
|
||||||
this._transact((y) => {
|
this._transact((y) => {
|
||||||
let c = this._map.get(key)
|
let c = this._map.get(key)
|
||||||
@ -52,11 +85,22 @@ export default class YMap extends Type {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or updates an element with a specified key and value.
|
||||||
|
*
|
||||||
|
* @param {encodable} key The key of the element to add to this YMap.
|
||||||
|
* @param {encodable | YType} value The value of the element to add to this
|
||||||
|
* YMap.
|
||||||
|
*/
|
||||||
set (key, value) {
|
set (key, value) {
|
||||||
this._transact(y => {
|
this._transact(y => {
|
||||||
const old = this._map.get(key) || null
|
const old = this._map.get(key) || null
|
||||||
if (old !== null) {
|
if (old !== null) {
|
||||||
if (old.constructor === ItemJSON && !old._deleted && old._content[0] === value) {
|
if (
|
||||||
|
old.constructor === ItemJSON &&
|
||||||
|
!old._deleted && old._content[0] === value
|
||||||
|
) {
|
||||||
// Trying to overwrite with same value
|
// Trying to overwrite with same value
|
||||||
// break here
|
// break here
|
||||||
return value
|
return value
|
||||||
@ -87,6 +131,12 @@ export default class YMap extends Type {
|
|||||||
})
|
})
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a specified element from this YMap.
|
||||||
|
*
|
||||||
|
* @param {encodable} key The key of the element to return.
|
||||||
|
*/
|
||||||
get (key) {
|
get (key) {
|
||||||
let v = this._map.get(key)
|
let v = this._map.get(key)
|
||||||
if (v === undefined || v._deleted) {
|
if (v === undefined || v._deleted) {
|
||||||
@ -98,6 +148,12 @@ export default class YMap extends Type {
|
|||||||
return v._content[v._content.length - 1]
|
return v._content[v._content.length - 1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a boolean indicating whether the specified key exists or not.
|
||||||
|
*
|
||||||
|
* @param {encodable} key The key to test.
|
||||||
|
*/
|
||||||
has (key) {
|
has (key) {
|
||||||
let v = this._map.get(key)
|
let v = this._map.get(key)
|
||||||
if (v === undefined || v._deleted) {
|
if (v === undefined || v._deleted) {
|
||||||
@ -106,6 +162,12 @@ export default class YMap extends Type {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Transform this YMap to a readable format.
|
||||||
|
* Useful for logging as all Items implement this method.
|
||||||
|
*/
|
||||||
_logString () {
|
_logString () {
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
const left = this._left !== null ? this._left._lastId : null
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
const origin = this._origin !== null ? this._origin._lastId : null
|
||||||
|
@ -46,11 +46,11 @@ function findNextPosition (currentAttributes, parent, left, right, count) {
|
|||||||
return [left, right, currentAttributes]
|
return [left, right, currentAttributes]
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPosition (parent, pos) {
|
function findPosition (parent, index) {
|
||||||
let currentAttributes = new Map()
|
let currentAttributes = new Map()
|
||||||
let left = null
|
let left = null
|
||||||
let right = parent._start
|
let right = parent._start
|
||||||
return findNextPosition(currentAttributes, parent, left, right, pos)
|
return findNextPosition(currentAttributes, parent, left, right, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
// negate applied formats
|
// negate applied formats
|
||||||
@ -212,11 +212,50 @@ function deleteText (y, length, parent, left, right, currentAttributes) {
|
|||||||
return [left, right]
|
return [left, right]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: In the quill delta representation we should also use the format {ops:[..]}
|
||||||
|
/**
|
||||||
|
* The Quill Delta format represents changes on a text document with
|
||||||
|
* formatting information. For mor information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* {
|
||||||
|
* ops: [
|
||||||
|
* { insert: 'Gandalf', attributes: { bold: true } },
|
||||||
|
* { insert: ' the ' },
|
||||||
|
* { insert: 'Grey', attributes: { color: '#cccccc' } }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @typedef {Array<Object>} Delta
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attributes that can be assigned to a selection of text.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* {
|
||||||
|
* bold: true,
|
||||||
|
* font-size: '40px'
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @typedef {Object} TextAttributes
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event that describes the changes on a YText type.
|
||||||
|
*/
|
||||||
class YTextEvent extends YArrayEvent {
|
class YTextEvent extends YArrayEvent {
|
||||||
constructor (ytext, remote, transaction) {
|
constructor (ytext, remote, transaction) {
|
||||||
super(ytext, remote, transaction)
|
super(ytext, remote, transaction)
|
||||||
this._delta = null
|
this._delta = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the changes in the delta format.
|
||||||
|
*
|
||||||
|
* @return {Delta} A {@link https://quilljs.com/docs/delta/|Quill Delta}) that
|
||||||
|
* represents the changes on the document.
|
||||||
|
*/
|
||||||
get delta () {
|
get delta () {
|
||||||
if (this._delta === null) {
|
if (this._delta === null) {
|
||||||
const y = this.target._y
|
const y = this.target._y
|
||||||
@ -378,6 +417,15 @@ class YTextEvent extends YArrayEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type that represents text with formatting information.
|
||||||
|
*
|
||||||
|
* This type replaces y-richtext as this implementation is able to handle
|
||||||
|
* block formats (format information on a paragraph), embeds (complex elements
|
||||||
|
* like pictures and videos), and text formats (**bold**, *italic*).
|
||||||
|
*
|
||||||
|
* @param {String} string The initial value of the YText.
|
||||||
|
*/
|
||||||
export default class YText extends YArray {
|
export default class YText extends YArray {
|
||||||
constructor (string) {
|
constructor (string) {
|
||||||
super()
|
super()
|
||||||
@ -388,9 +436,18 @@ export default class YText extends YArray {
|
|||||||
this._start = start
|
this._start = start
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Creates YMap Event and calls observers.
|
||||||
|
*/
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
_callObserver (transaction, parentSubs, remote) {
|
||||||
this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
|
this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the unformatted string representation of this YText type.
|
||||||
|
*/
|
||||||
toString () {
|
toString () {
|
||||||
let str = ''
|
let str = ''
|
||||||
let n = this._start
|
let n = this._start
|
||||||
@ -402,6 +459,12 @@ export default class YText extends YArray {
|
|||||||
}
|
}
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a {@link Delta} on this shared YText type.
|
||||||
|
*
|
||||||
|
* @param {Delta} delta The changes to apply on this element.
|
||||||
|
*/
|
||||||
applyDelta (delta) {
|
applyDelta (delta) {
|
||||||
this._transact(y => {
|
this._transact(y => {
|
||||||
let left = null
|
let left = null
|
||||||
@ -419,8 +482,11 @@ export default class YText extends YArray {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As defined by Quilljs - https://quilljs.com/docs/delta/
|
* Returns the Delta representation of this YText type.
|
||||||
|
*
|
||||||
|
* @return {Delta} The Delta representation of this type.
|
||||||
*/
|
*/
|
||||||
toDelta () {
|
toDelta () {
|
||||||
let ops = []
|
let ops = []
|
||||||
@ -461,42 +527,84 @@ export default class YText extends YArray {
|
|||||||
packStr()
|
packStr()
|
||||||
return ops
|
return ops
|
||||||
}
|
}
|
||||||
insert (pos, text, attributes = {}) {
|
|
||||||
|
/**
|
||||||
|
* Insert text at a given index.
|
||||||
|
*
|
||||||
|
* @param {Integer} index The index at which to start inserting.
|
||||||
|
* @param {String} text The text to insert at the specified position.
|
||||||
|
* @param {TextAttributes} attributes Optionally define some formatting
|
||||||
|
* information to apply on the inserted
|
||||||
|
* Text.
|
||||||
|
*/
|
||||||
|
insert (index, text, attributes = {}) {
|
||||||
if (text.length <= 0) {
|
if (text.length <= 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this._transact(y => {
|
this._transact(y => {
|
||||||
let [left, right, currentAttributes] = findPosition(this, pos)
|
let [left, right, currentAttributes] = findPosition(this, index)
|
||||||
insertText(y, text, this, left, right, currentAttributes, attributes)
|
insertText(y, text, this, left, right, currentAttributes, attributes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
insertEmbed (pos, embed, attributes = {}) {
|
|
||||||
|
/**
|
||||||
|
* Inserts an embed at a index.
|
||||||
|
*
|
||||||
|
* @param {Integer} index The index to insert the embed at.
|
||||||
|
* @param {Object} embed The Object that represents the embed.
|
||||||
|
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||||
|
* embed
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
insertEmbed (index, embed, attributes = {}) {
|
||||||
if (embed.constructor !== Object) {
|
if (embed.constructor !== Object) {
|
||||||
throw new Error('Embed must be an Object')
|
throw new Error('Embed must be an Object')
|
||||||
}
|
}
|
||||||
this._transact(y => {
|
this._transact(y => {
|
||||||
let [left, right, currentAttributes] = findPosition(this, pos)
|
let [left, right, currentAttributes] = findPosition(this, index)
|
||||||
insertText(y, embed, this, left, right, currentAttributes, attributes)
|
insertText(y, embed, this, left, right, currentAttributes, attributes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
delete (pos, length) {
|
|
||||||
|
/**
|
||||||
|
* Deletes text starting from an index.
|
||||||
|
*
|
||||||
|
* @param {Integer} index Index at which to start deleting.
|
||||||
|
* @param {Integer} length The number of characters to remove. Defaults to 1.
|
||||||
|
*/
|
||||||
|
delete (index, length) {
|
||||||
if (length === 0) {
|
if (length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this._transact(y => {
|
this._transact(y => {
|
||||||
let [left, right, currentAttributes] = findPosition(this, pos)
|
let [left, right, currentAttributes] = findPosition(this, index)
|
||||||
deleteText(y, length, this, left, right, currentAttributes)
|
deleteText(y, length, this, left, right, currentAttributes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
format (pos, length, attributes) {
|
|
||||||
|
/**
|
||||||
|
* Assigns properties to a range of text.
|
||||||
|
*
|
||||||
|
* @param {Integer} index The position where to start formatting.
|
||||||
|
* @param {Integer} length The amount of characters to assign properties to.
|
||||||
|
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||||
|
* text.
|
||||||
|
*/
|
||||||
|
format (index, length, attributes) {
|
||||||
this._transact(y => {
|
this._transact(y => {
|
||||||
let [left, right, currentAttributes] = findPosition(this, pos)
|
let [left, right, currentAttributes] = findPosition(this, index)
|
||||||
if (right === null) {
|
if (right === null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
formatText(y, length, this, left, right, currentAttributes, attributes)
|
formatText(y, length, this, left, right, currentAttributes, attributes)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Transform this YText to a readable format.
|
||||||
|
* Useful for logging as all Items implement this method.
|
||||||
|
*/
|
||||||
_logString () {
|
_logString () {
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
const left = this._left !== null ? this._left._lastId : null
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
const origin = this._origin !== null ? this._origin._lastId : null
|
||||||
|
@ -3,6 +3,16 @@ import { defaultDomFilter } from './utils.js'
|
|||||||
import YMap from '../YMap.js'
|
import YMap from '../YMap.js'
|
||||||
import { YXmlFragment } from './y-xml.js'
|
import { YXmlFragment } from './y-xml.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An YXmlElement imitates the behavior of a
|
||||||
|
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
|
||||||
|
*
|
||||||
|
* * An YXmlElement has attributes (key value pairs)
|
||||||
|
* * An YXmlElement has childElements that must inherit from YXmlElement
|
||||||
|
*
|
||||||
|
* @param {String} arg1 Node name
|
||||||
|
* @param {Function} arg2 Dom filter
|
||||||
|
*/
|
||||||
export default class YXmlElement extends YXmlFragment {
|
export default class YXmlElement extends YXmlFragment {
|
||||||
constructor (arg1, arg2, _document) {
|
constructor (arg1, arg2, _document) {
|
||||||
super()
|
super()
|
||||||
@ -20,11 +30,21 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
this._domFilter = arg2
|
this._domFilter = arg2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Creates an Item with the same effect as this Item (without position effect)
|
||||||
|
*/
|
||||||
_copy () {
|
_copy () {
|
||||||
let struct = super._copy()
|
let struct = super._copy()
|
||||||
struct.nodeName = this.nodeName
|
struct.nodeName = this.nodeName
|
||||||
return struct
|
return struct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Copies children and attributes from a dom node to this YXmlElement.
|
||||||
|
*/
|
||||||
_setDom (dom, _document) {
|
_setDom (dom, _document) {
|
||||||
if (this._dom != null) {
|
if (this._dom != null) {
|
||||||
throw new Error('Only call this method if you know what you are doing ;)')
|
throw new Error('Only call this method if you know what you are doing ;)')
|
||||||
@ -48,20 +68,61 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
return dom
|
return dom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Bind a dom to to this YXmlElement. This means that the DOM changes when the
|
||||||
|
* YXmlElement is modified and that this YXmlElement changes when the DOM is
|
||||||
|
* modified.
|
||||||
|
*
|
||||||
|
* Currently only works in YXmlFragment.
|
||||||
|
*/
|
||||||
_bindToDom (dom, _document) {
|
_bindToDom (dom, _document) {
|
||||||
_document = _document || document
|
_document = _document || document
|
||||||
this._dom = dom
|
this._dom = dom
|
||||||
dom._yxml = this
|
dom._yxml = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Read the next Item in a Decoder and fill this Item with the read data.
|
||||||
|
*
|
||||||
|
* This is called when data is received from a remote peer.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||||
|
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||||
|
*/
|
||||||
_fromBinary (y, decoder) {
|
_fromBinary (y, decoder) {
|
||||||
const missing = super._fromBinary(y, decoder)
|
const missing = super._fromBinary(y, decoder)
|
||||||
this.nodeName = decoder.readVarString()
|
this.nodeName = decoder.readVarString()
|
||||||
return missing
|
return missing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||||
|
*/
|
||||||
_toBinary (encoder) {
|
_toBinary (encoder) {
|
||||||
super._toBinary(encoder)
|
super._toBinary(encoder)
|
||||||
encoder.writeVarString(this.nodeName)
|
encoder.writeVarString(this.nodeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Integrates this Item into the shared structure.
|
||||||
|
*
|
||||||
|
* This method actually applies the change to the Yjs instance. In case of
|
||||||
|
* Item it connects _left and _right to this Item and calls the
|
||||||
|
* {@link Item#beforeChange} method.
|
||||||
|
*
|
||||||
|
* * Checks for nodeName
|
||||||
|
* * Sets domFilter
|
||||||
|
*/
|
||||||
_integrate (y) {
|
_integrate (y) {
|
||||||
if (this.nodeName === null) {
|
if (this.nodeName === null) {
|
||||||
throw new Error('nodeName must be defined!')
|
throw new Error('nodeName must be defined!')
|
||||||
@ -71,8 +132,9 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
}
|
}
|
||||||
super._integrate(y)
|
super._integrate(y)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the string representation of the XML document.
|
* Returns the string representation 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
|
||||||
*/
|
*/
|
||||||
@ -93,18 +155,42 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
|
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
|
||||||
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
|
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an attribute from this YXmlElement.
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name that is to be removed.
|
||||||
|
*/
|
||||||
removeAttribute () {
|
removeAttribute () {
|
||||||
return YMap.prototype.delete.apply(this, arguments)
|
return YMap.prototype.delete.apply(this, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets or updates an attribute.
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name that is to be set.
|
||||||
|
* @param {String} attributeValue The attribute value that is to be set.
|
||||||
|
*/
|
||||||
setAttribute () {
|
setAttribute () {
|
||||||
return YMap.prototype.set.apply(this, arguments)
|
return YMap.prototype.set.apply(this, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an attribute value that belongs to the attribute name.
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name that identifies the
|
||||||
|
* queried value.
|
||||||
|
* @return {String} The queried attribute value
|
||||||
|
*/
|
||||||
getAttribute () {
|
getAttribute () {
|
||||||
return YMap.prototype.get.apply(this, arguments)
|
return YMap.prototype.get.apply(this, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all attribute name/value pairs in a JSON Object.
|
||||||
|
*
|
||||||
|
* @return {Object} A JSON Object that describes the attributes.
|
||||||
|
*/
|
||||||
getAttributes () {
|
getAttributes () {
|
||||||
const obj = {}
|
const obj = {}
|
||||||
for (let [key, value] of this._map) {
|
for (let [key, value] of this._map) {
|
||||||
@ -114,6 +200,12 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Dom Element that mirrors this YXmlElement.
|
||||||
|
*
|
||||||
|
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||||
|
*/
|
||||||
getDom (_document) {
|
getDom (_document) {
|
||||||
_document = _document || document
|
_document = _document || document
|
||||||
let dom = this._dom
|
let dom = this._dom
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import YEvent from '../../Util/YEvent.js'
|
import YEvent from '../../Util/YEvent.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An Event that describes changes on a YXml Element or Yxml Fragment
|
||||||
|
*/
|
||||||
export default class YXmlEvent extends YEvent {
|
export default class YXmlEvent extends YEvent {
|
||||||
constructor (target, subs, remote) {
|
constructor (target, subs, remote) {
|
||||||
super(target)
|
super(target)
|
||||||
|
@ -36,6 +36,24 @@ function domToYXml (parent, doms, _document) {
|
|||||||
return types
|
return types
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
|
||||||
|
* position within them.
|
||||||
|
*
|
||||||
|
* Can be created with {@link YXmlFragment#createTreeWalker}
|
||||||
|
*/
|
||||||
class YXmlTreeWalker {
|
class YXmlTreeWalker {
|
||||||
constructor (root, f) {
|
constructor (root, f) {
|
||||||
this._filter = f || (() => true)
|
this._filter = f || (() => true)
|
||||||
@ -46,6 +64,11 @@ class YXmlTreeWalker {
|
|||||||
[Symbol.iterator] () {
|
[Symbol.iterator] () {
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Get the next node.
|
||||||
|
*
|
||||||
|
* @return {YXmlElement} The next node.
|
||||||
|
*/
|
||||||
next () {
|
next () {
|
||||||
let n = this._currentNode
|
let n = this._currentNode
|
||||||
if (this._firstCall) {
|
if (this._firstCall) {
|
||||||
@ -84,6 +107,11 @@ class YXmlTreeWalker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a list of {@link YXmlElement}.
|
||||||
|
* A YxmlFragment does not have a nodeName and it does not have attributes.
|
||||||
|
* Therefore it also must not be added as a childElement.
|
||||||
|
*/
|
||||||
export default class YXmlFragment extends YArray {
|
export default class YXmlFragment extends YArray {
|
||||||
constructor () {
|
constructor () {
|
||||||
super()
|
super()
|
||||||
@ -110,18 +138,31 @@ export default class YXmlFragment extends YArray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a subtree of childNodes.
|
||||||
|
*
|
||||||
|
* @param {Function} 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 {TreeWalker} A subtree and a position within it.
|
||||||
|
*/
|
||||||
createTreeWalker (filter) {
|
createTreeWalker (filter) {
|
||||||
return new YXmlTreeWalker(this, filter)
|
return new YXmlTreeWalker(this, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve first element that matches *query*
|
* Returns the first YXmlElement that matches the query.
|
||||||
* Similar to DOM's querySelector, but only accepts a subset of its queries
|
* Similar to DOM's {@link querySelector}.
|
||||||
*
|
*
|
||||||
* Query support:
|
* Query support:
|
||||||
* - tagname
|
* - tagname
|
||||||
* TODO:
|
* TODO:
|
||||||
* - id
|
* - id
|
||||||
* - attribute
|
* - attribute
|
||||||
|
*
|
||||||
|
* @param {CSS_Selector} query The query on the children.
|
||||||
|
* @return {?YXmlElement} The first element that matches the query or null.
|
||||||
*/
|
*/
|
||||||
querySelector (query) {
|
querySelector (query) {
|
||||||
query = query.toUpperCase()
|
query = query.toUpperCase()
|
||||||
@ -133,16 +174,52 @@ export default class YXmlFragment extends YArray {
|
|||||||
return next.value
|
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>} The elements that match this query.
|
||||||
|
*/
|
||||||
querySelectorAll (query) {
|
querySelectorAll (query) {
|
||||||
query = query.toUpperCase()
|
query = query.toUpperCase()
|
||||||
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
|
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables the smart scrolling functionality for a Dom Binding.
|
||||||
|
* This is useful when YXml is bound to a shared editor. When activated,
|
||||||
|
* the viewport will be changed to accommodate remote changes.
|
||||||
|
*
|
||||||
|
* @TODO: Disabled for now.
|
||||||
|
*
|
||||||
|
* @param {Element} scrollElement The node that is
|
||||||
|
*/
|
||||||
enableSmartScrolling (scrollElement) {
|
enableSmartScrolling (scrollElement) {
|
||||||
this._scrollElement = scrollElement
|
this._scrollElement = scrollElement
|
||||||
this.forEach(xml => {
|
this.forEach(xml => {
|
||||||
xml.enableSmartScrolling(scrollElement)
|
xml.enableSmartScrolling(scrollElement)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter out Dom elements.
|
||||||
|
*
|
||||||
|
* @param {domFilter} f The filtering function that decides whether to include
|
||||||
|
* a Dom node.
|
||||||
|
*/
|
||||||
setDomFilter (f) {
|
setDomFilter (f) {
|
||||||
this._domFilter = f
|
this._domFilter = f
|
||||||
let attributes = new Map()
|
let attributes = new Map()
|
||||||
@ -168,16 +245,41 @@ export default class YXmlFragment extends YArray {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Creates YArray Event and calls observers.
|
||||||
|
*/
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
_callObserver (transaction, parentSubs, remote) {
|
||||||
this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote))
|
this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the string representation of all the children of this YXmlFragment.
|
||||||
|
*
|
||||||
|
* @return {string} The string representation of all children.
|
||||||
|
*/
|
||||||
toString () {
|
toString () {
|
||||||
return this.map(xml => xml.toString()).join('')
|
return this.map(xml => xml.toString()).join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Unbind from Dom and mark this Item as deleted.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance
|
||||||
|
* @param {boolean} createDelete Whether to propagate a message that this
|
||||||
|
* Type was deleted.
|
||||||
|
*/
|
||||||
_delete (y, createDelete) {
|
_delete (y, createDelete) {
|
||||||
this._unbindFromDom()
|
this._unbindFromDom()
|
||||||
super._delete(y, createDelete)
|
super._delete(y, createDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Unbind this YXmlFragment from the Dom.
|
||||||
|
*/
|
||||||
_unbindFromDom () {
|
_unbindFromDom () {
|
||||||
if (this._domObserver != null) {
|
if (this._domObserver != null) {
|
||||||
this._domObserver.disconnect()
|
this._domObserver.disconnect()
|
||||||
@ -191,19 +293,53 @@ export default class YXmlFragment extends YArray {
|
|||||||
this._y.off('beforeTransaction', this._beforeTransactionHandler)
|
this._y.off('beforeTransaction', this._beforeTransactionHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert Dom Elements after one of the children of this YXmlFragment.
|
||||||
|
* The Dom elements will be bound to a new YXmlElement and inserted at the
|
||||||
|
* specified position.
|
||||||
|
*
|
||||||
|
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
||||||
|
* inserted after this node. Set null to insert at
|
||||||
|
* the beginning.
|
||||||
|
* @param {Array<Element>} doms The Dom elements to insert.
|
||||||
|
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
|
||||||
|
*/
|
||||||
insertDomElementsAfter (prev, doms, _document) {
|
insertDomElementsAfter (prev, doms, _document) {
|
||||||
const types = domToYXml(this, doms, _document)
|
const types = domToYXml(this, doms, _document)
|
||||||
this.insertAfter(prev, types)
|
this.insertAfter(prev, types)
|
||||||
return types
|
return types
|
||||||
}
|
}
|
||||||
insertDomElements (pos, doms, _document) {
|
|
||||||
|
/**
|
||||||
|
* Insert Dom Elements at a specified index.
|
||||||
|
* The Dom elements will be bound to a new YXmlElement and inserted at the
|
||||||
|
* specified position.
|
||||||
|
*
|
||||||
|
* @param {Integer} index The position to insert elements at.
|
||||||
|
* @param {Array<Element>} doms The Dom elements to insert.
|
||||||
|
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
|
||||||
|
*/
|
||||||
|
insertDomElements (index, doms, _document) {
|
||||||
const types = domToYXml(this, doms, _document)
|
const types = domToYXml(this, doms, _document)
|
||||||
this.insert(pos, types)
|
this.insert(index, types)
|
||||||
return types
|
return types
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Dom representation of this YXml type..
|
||||||
|
*/
|
||||||
getDom () {
|
getDom () {
|
||||||
return this._dom
|
return this._dom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind this YXmlFragment and all its children to a Dom Element.
|
||||||
|
* The content of the Dom Element are replaced with the Dom representation of
|
||||||
|
* the children of this YXml Type.
|
||||||
|
*
|
||||||
|
* @param {Element} dom The Dom Element that should be bound to this Type.
|
||||||
|
*/
|
||||||
bindToDom (dom, _document) {
|
bindToDom (dom, _document) {
|
||||||
if (this._dom != null) {
|
if (this._dom != null) {
|
||||||
this._unbindFromDom()
|
this._unbindFromDom()
|
||||||
@ -217,8 +353,12 @@ export default class YXmlFragment extends YArray {
|
|||||||
})
|
})
|
||||||
this._bindToDom(dom, _document)
|
this._bindToDom(dom, _document)
|
||||||
}
|
}
|
||||||
// binds to a dom element
|
|
||||||
// Only call if dom and YXml are isomorph
|
/**
|
||||||
|
* @private
|
||||||
|
* Binds to a dom element.
|
||||||
|
* Only call if dom and YXml are isomorph
|
||||||
|
*/
|
||||||
_bindToDom (dom, _document) {
|
_bindToDom (dom, _document) {
|
||||||
_document = _document || document
|
_document = _document || document
|
||||||
this._dom = dom
|
this._dom = dom
|
||||||
@ -346,6 +486,12 @@ export default class YXmlFragment extends YArray {
|
|||||||
}
|
}
|
||||||
return dom
|
return dom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Transform this YXml Type to a readable format.
|
||||||
|
* Useful for logging as all Items implement this method.
|
||||||
|
*/
|
||||||
_logString () {
|
_logString () {
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
const left = this._left !== null ? this._left._lastId : null
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
const origin = this._origin !== null ? this._origin._lastId : null
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import YMap from '../YMap.js'
|
import YMap from '../YMap.js'
|
||||||
import { getHook, addHook } from './hooks.js'
|
import { getHook, addHook } from './hooks.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can manage binding to a custom type with YXmlHook.
|
||||||
|
*
|
||||||
|
* @param {String} hookName nodeName of the Dom Node.
|
||||||
|
*/
|
||||||
export default class YXmlHook extends YMap {
|
export default class YXmlHook extends YMap {
|
||||||
constructor (hookName, dom) {
|
constructor (hookName, dom) {
|
||||||
super()
|
super()
|
||||||
@ -14,11 +19,20 @@ export default class YXmlHook extends YMap {
|
|||||||
getHook(hookName).fillType(dom, this)
|
getHook(hookName).fillType(dom, this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Creates an Item with the same effect as this Item (without position effect)
|
||||||
|
*/
|
||||||
_copy () {
|
_copy () {
|
||||||
const struct = super._copy()
|
const struct = super._copy()
|
||||||
struct.hookName = this.hookName
|
struct.hookName = this.hookName
|
||||||
return struct
|
return struct
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Dom representation of this YXmlHook.
|
||||||
|
*/
|
||||||
getDom (_document) {
|
getDom (_document) {
|
||||||
_document = _document || document
|
_document = _document || document
|
||||||
if (this._dom === null) {
|
if (this._dom === null) {
|
||||||
@ -29,20 +43,56 @@ export default class YXmlHook extends YMap {
|
|||||||
}
|
}
|
||||||
return this._dom
|
return this._dom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Removes the Dom binding.
|
||||||
|
*/
|
||||||
_unbindFromDom () {
|
_unbindFromDom () {
|
||||||
this._dom._yxml = null
|
this._dom._yxml = null
|
||||||
this._yxml = null
|
this._yxml = null
|
||||||
// TODO: cleanup hook?
|
// TODO: cleanup hook?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Read the next Item in a Decoder and fill this Item with the read data.
|
||||||
|
*
|
||||||
|
* This is called when data is received from a remote peer.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance that this Item belongs to.
|
||||||
|
* @param {BinaryDecoder} decoder The decoder object to read data from.
|
||||||
|
*/
|
||||||
_fromBinary (y, decoder) {
|
_fromBinary (y, decoder) {
|
||||||
const missing = super._fromBinary(y, decoder)
|
const missing = super._fromBinary(y, decoder)
|
||||||
this.hookName = decoder.readVarString()
|
this.hookName = decoder.readVarString()
|
||||||
return missing
|
return missing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param {BinaryEncoder} encoder The encoder to write data to.
|
||||||
|
*/
|
||||||
_toBinary (encoder) {
|
_toBinary (encoder) {
|
||||||
super._toBinary(encoder)
|
super._toBinary(encoder)
|
||||||
encoder.writeVarString(this.hookName)
|
encoder.writeVarString(this.hookName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* 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 {Y} y The Yjs instance
|
||||||
|
*/
|
||||||
_integrate (y) {
|
_integrate (y) {
|
||||||
if (this.hookName === null) {
|
if (this.hookName === null) {
|
||||||
throw new Error('hookName must be defined!')
|
throw new Error('hookName must be defined!')
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import YText from '../YText.js'
|
import YText from '../YText.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents text in a Dom Element. In the future this type will also handle
|
||||||
|
* simple formatting information like bold and italic.
|
||||||
|
*
|
||||||
|
* @param {String} arg1 Initial value.
|
||||||
|
*/
|
||||||
export default class YXmlText extends YText {
|
export default class YXmlText extends YText {
|
||||||
constructor (arg1) {
|
constructor (arg1) {
|
||||||
let dom = null
|
let dom = null
|
||||||
@ -56,6 +62,15 @@ export default class YXmlText extends YText {
|
|||||||
enableSmartScrolling (scrollElement) {
|
enableSmartScrolling (scrollElement) {
|
||||||
this._scrollElement = scrollElement
|
this._scrollElement = scrollElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Set Dom element / Text Node that represents the same content as this
|
||||||
|
* YXmlElement.
|
||||||
|
*
|
||||||
|
* @param {Element} dom The Dom Element / Text Node that is set to be
|
||||||
|
* equivalent to this Type.
|
||||||
|
*/
|
||||||
_setDom (dom) {
|
_setDom (dom) {
|
||||||
if (this._dom != null) {
|
if (this._dom != null) {
|
||||||
this._unbindFromDom()
|
this._unbindFromDom()
|
||||||
@ -67,6 +82,10 @@ export default class YXmlText extends YText {
|
|||||||
this._dom = dom
|
this._dom = dom
|
||||||
dom._yxml = this
|
dom._yxml = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Dom representation of this YXmlText.
|
||||||
|
*/
|
||||||
getDom (_document) {
|
getDom (_document) {
|
||||||
_document = _document || document
|
_document = _document || document
|
||||||
if (this._dom === null) {
|
if (this._dom === null) {
|
||||||
@ -76,10 +95,24 @@ export default class YXmlText extends YText {
|
|||||||
}
|
}
|
||||||
return this._dom
|
return this._dom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Mark this Item as deleted.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance
|
||||||
|
* @param {boolean} createDelete Whether to propagate a message that this
|
||||||
|
* Type was deleted.
|
||||||
|
*/
|
||||||
_delete (y, createDelete) {
|
_delete (y, createDelete) {
|
||||||
this._unbindFromDom()
|
this._unbindFromDom()
|
||||||
super._delete(y, createDelete)
|
super._delete(y, createDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Unbind this YXmlText from the Dom.
|
||||||
|
*/
|
||||||
_unbindFromDom () {
|
_unbindFromDom () {
|
||||||
if (this._domObserver != null) {
|
if (this._domObserver != null) {
|
||||||
this._domObserver.disconnect()
|
this._domObserver.disconnect()
|
||||||
|
@ -184,8 +184,8 @@ export function reflectChangesOnDom (events, _document) {
|
|||||||
dom.setAttribute(attributeName, value)
|
dom.setAttribute(attributeName, value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
/**
|
/*
|
||||||
* TODO: instead of chard-checking the types, it would be best to
|
* TODO: instead of hard-checking the types, it would be best to
|
||||||
* specify the type's features. E.g.
|
* specify the type's features. E.g.
|
||||||
* - _yxmlHasAttributes
|
* - _yxmlHasAttributes
|
||||||
* - _yxmlHasChildren
|
* - _yxmlHasChildren
|
||||||
|
@ -1,22 +1,56 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* General event handler implementation.
|
||||||
|
*/
|
||||||
export default class EventHandler {
|
export default class EventHandler {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.eventListeners = []
|
this.eventListeners = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To prevent memory leaks, call this method when the eventListeners won't be
|
||||||
|
* used anymore.
|
||||||
|
*/
|
||||||
destroy () {
|
destroy () {
|
||||||
this.eventListeners = null
|
this.eventListeners = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an event listener that is called when
|
||||||
|
* {@link EventHandler#callEventListeners} is called.
|
||||||
|
*
|
||||||
|
* @param {Function} f The event handler.
|
||||||
|
*/
|
||||||
addEventListener (f) {
|
addEventListener (f) {
|
||||||
this.eventListeners.push(f)
|
this.eventListeners.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an event listener.
|
||||||
|
*
|
||||||
|
* @param {Function} f The event handler that was added with
|
||||||
|
* {@link EventHandler#addEventListener}
|
||||||
|
*/
|
||||||
removeEventListener (f) {
|
removeEventListener (f) {
|
||||||
this.eventListeners = this.eventListeners.filter(function (g) {
|
this.eventListeners = this.eventListeners.filter(function (g) {
|
||||||
return f !== g
|
return f !== g
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all event listeners.
|
||||||
|
*/
|
||||||
removeAllEventListeners () {
|
removeAllEventListeners () {
|
||||||
this.eventListeners = []
|
this.eventListeners = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call all event listeners that were added via
|
||||||
|
* {@link EventHandler#addEventListener}.
|
||||||
|
*
|
||||||
|
* @param {Transaction} transaction The transaction object // TODO: do we need this?
|
||||||
|
* @param {YEvent} event An event object that describes the change on a type.
|
||||||
|
*/
|
||||||
callEventListeners (transaction, event) {
|
callEventListeners (transaction, event) {
|
||||||
for (var i = 0; i < this.eventListeners.length; i++) {
|
for (var i = 0; i < this.eventListeners.length; i++) {
|
||||||
try {
|
try {
|
||||||
|
@ -1,8 +1,19 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Handles named events.
|
||||||
|
*/
|
||||||
export default class NamedEventHandler {
|
export default class NamedEventHandler {
|
||||||
constructor () {
|
constructor () {
|
||||||
this._eventListener = new Map()
|
this._eventListener = new Map()
|
||||||
this._stateListener = new Map()
|
this._stateListener = new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Returns all listeners that listen to a specified name.
|
||||||
|
*
|
||||||
|
* @param {String} name The query event name.
|
||||||
|
*/
|
||||||
_getListener (name) {
|
_getListener (name) {
|
||||||
let listeners = this._eventListener.get(name)
|
let listeners = this._eventListener.get(name)
|
||||||
if (listeners === undefined) {
|
if (listeners === undefined) {
|
||||||
@ -14,14 +25,34 @@ export default class NamedEventHandler {
|
|||||||
}
|
}
|
||||||
return listeners
|
return listeners
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a named event listener. The listener is removed after it has been
|
||||||
|
* called once.
|
||||||
|
*
|
||||||
|
* @param {String} name The event name to listen to.
|
||||||
|
* @param {Function} f The function that is executed when the event is fired.
|
||||||
|
*/
|
||||||
once (name, f) {
|
once (name, f) {
|
||||||
let listeners = this._getListener(name)
|
let listeners = this._getListener(name)
|
||||||
listeners.once.add(f)
|
listeners.once.add(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a named event listener.
|
||||||
|
*
|
||||||
|
* @param {String} name The event name to listen to.
|
||||||
|
* @param {Function} f The function that is executed when the event is fired.
|
||||||
|
*/
|
||||||
on (name, f) {
|
on (name, f) {
|
||||||
let listeners = this._getListener(name)
|
let listeners = this._getListener(name)
|
||||||
listeners.on.add(f)
|
listeners.on.add(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Init the saved state for an event name.
|
||||||
|
*/
|
||||||
_initStateListener (name) {
|
_initStateListener (name) {
|
||||||
let state = this._stateListener.get(name)
|
let state = this._stateListener.get(name)
|
||||||
if (state === undefined) {
|
if (state === undefined) {
|
||||||
@ -33,9 +64,20 @@ export default class NamedEventHandler {
|
|||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a Promise that is resolved when the event name is called.
|
||||||
|
* The Promise is immediately resolved when the event name was called in the
|
||||||
|
* past.
|
||||||
|
*/
|
||||||
when (name) {
|
when (name) {
|
||||||
return this._initStateListener(name).promise
|
return this._initStateListener(name).promise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an event listener that was registered with either
|
||||||
|
* {@link EventHandler#on} or {@link EventHandler#once}.
|
||||||
|
*/
|
||||||
off (name, f) {
|
off (name, f) {
|
||||||
if (name == null || f == null) {
|
if (name == null || f == null) {
|
||||||
throw new Error('You must specify event name and function!')
|
throw new Error('You must specify event name and function!')
|
||||||
@ -46,6 +88,14 @@ export default class NamedEventHandler {
|
|||||||
listener.once.delete(f)
|
listener.once.delete(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a named event. All registered event listeners that listen to the
|
||||||
|
* specified name will receive the event.
|
||||||
|
*
|
||||||
|
* @param {String} name The event name.
|
||||||
|
* @param {Array} args The arguments that are applied to the event listener.
|
||||||
|
*/
|
||||||
emit (name, ...args) {
|
emit (name, ...args) {
|
||||||
this._initStateListener(name).resolve()
|
this._initStateListener(name).resolve()
|
||||||
const listener = this._eventListener.get(name)
|
const listener = this._eventListener.get(name)
|
||||||
|
@ -64,7 +64,15 @@ function applyReverseOperation (y, scope, reverseBuffer) {
|
|||||||
return performedUndo
|
return performedUndo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a history of locally applied operations. The UndoManager handles the
|
||||||
|
* undoing and redoing of locally created changes.
|
||||||
|
*/
|
||||||
export default class UndoManager {
|
export default class UndoManager {
|
||||||
|
/**
|
||||||
|
* @param {YType} scope The scope on which to listen for changes.
|
||||||
|
* @param {Object} options Optionally provided configuration.
|
||||||
|
*/
|
||||||
constructor (scope, options = {}) {
|
constructor (scope, options = {}) {
|
||||||
this.options = options
|
this.options = options
|
||||||
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
|
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
|
||||||
@ -101,12 +109,20 @@ export default class UndoManager {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo the last locally created change.
|
||||||
|
*/
|
||||||
undo () {
|
undo () {
|
||||||
this._undoing = true
|
this._undoing = true
|
||||||
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer)
|
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer)
|
||||||
this._undoing = false
|
this._undoing = false
|
||||||
return performedUndo
|
return performedUndo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redo the last locally created change.
|
||||||
|
*/
|
||||||
redo () {
|
redo () {
|
||||||
this._redoing = true
|
this._redoing = true
|
||||||
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer)
|
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer)
|
||||||
|
@ -1,9 +1,27 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* YEvent describes the changes on a YType.
|
||||||
|
*/
|
||||||
export default class YEvent {
|
export default class YEvent {
|
||||||
|
/**
|
||||||
|
* @param {YType} target The changed type.
|
||||||
|
*/
|
||||||
constructor (target) {
|
constructor (target) {
|
||||||
this.target = target
|
this.target = target
|
||||||
this.currentTarget = target
|
this.currentTarget = target
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the path from `y` to the changed type.
|
||||||
|
*
|
||||||
|
* The following property holds:
|
||||||
|
* @example
|
||||||
|
* let type = y
|
||||||
|
* event.path.forEach(function (dir) {
|
||||||
|
* type = type.get(dir)
|
||||||
|
* })
|
||||||
|
* type === event.target // => true
|
||||||
|
*/
|
||||||
get path () {
|
get path () {
|
||||||
const path = []
|
const path = []
|
||||||
let type = this.target
|
let type = this.target
|
||||||
|
@ -1,7 +1,43 @@
|
|||||||
import ID from './ID.js'
|
import ID from './ID.js'
|
||||||
import RootID from './RootID.js'
|
import RootID from './RootID.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A relative position that is based on the Yjs model. In contrast to an
|
||||||
|
* absolute position (position by index), the relative position can be
|
||||||
|
* recomputed when remote changes are received. For example:
|
||||||
|
*
|
||||||
|
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the cursor position.
|
||||||
|
*
|
||||||
|
* A relative cursor position can be obtained with the function
|
||||||
|
* {@link getRelativePosition} and it can be transformed to an absolute position
|
||||||
|
* with {@link fromRelativePosition}.
|
||||||
|
*
|
||||||
|
* Pro tip: Use this to implement shared cursor locations in YText or YXml!
|
||||||
|
* The relative position is {@link encodable}, so you can send it to other
|
||||||
|
* clients.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Current cursor position is at position 10
|
||||||
|
* let relativePosition = getRelativePosition(yText, 10)
|
||||||
|
* // modify yText
|
||||||
|
* yText.insert(0, 'abc')
|
||||||
|
* yText.delete(3, 10)
|
||||||
|
* // Compute the cursor position
|
||||||
|
* let absolutePosition = fromRelativePosition(y, relativePosition)
|
||||||
|
* absolutePosition.type // => yText
|
||||||
|
* console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3
|
||||||
|
*
|
||||||
|
* @typedef {encodable} RelativePosition
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a relativePosition based on a absolute position.
|
||||||
|
*
|
||||||
|
* @param {YType} type The base type (e.g. YText or YArray).
|
||||||
|
* @param {Integer} offset The absolute position.
|
||||||
|
*/
|
||||||
export function getRelativePosition (type, offset) {
|
export function getRelativePosition (type, offset) {
|
||||||
|
// TODO: rename to createRelativePosition
|
||||||
let t = type._start
|
let t = type._start
|
||||||
while (t !== null) {
|
while (t !== null) {
|
||||||
if (t._deleted === false) {
|
if (t._deleted === false) {
|
||||||
@ -15,6 +51,20 @@ export function getRelativePosition (type, offset) {
|
|||||||
return ['endof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null]
|
return ['endof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} AbsolutePosition The result of {@link fromRelativePosition}
|
||||||
|
* @property {YType} type The type on which to apply the absolute position.
|
||||||
|
* @property {Integer} offset The absolute offset.r
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a relative position back to a relative position.
|
||||||
|
*
|
||||||
|
* @param {Y} y The Yjs instance in which to query for the absolute position.
|
||||||
|
* @param {RelativePosition} rpos The relative position.
|
||||||
|
* @return {AbsolutePosition} The absolute position in the Yjs model
|
||||||
|
* (type + offset).
|
||||||
|
*/
|
||||||
export function fromRelativePosition (y, rpos) {
|
export function fromRelativePosition (y, rpos) {
|
||||||
if (rpos[0] === 'endof') {
|
if (rpos[0] === 'endof') {
|
||||||
let id
|
let id
|
||||||
|
111
src/Y.js
111
src/Y.js
@ -27,9 +27,32 @@ import QuillBinding from './Binding/QuillBinding.js'
|
|||||||
|
|
||||||
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
|
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anything that can be encoded with `JSON.stringify` and can be decoded with
|
||||||
|
* `JSON.parse`.
|
||||||
|
*
|
||||||
|
* The following property should hold:
|
||||||
|
* `JSON.parse(JSON.stringify(key))===key`
|
||||||
|
*
|
||||||
|
* At the moment the only safe values are number and string.
|
||||||
|
*
|
||||||
|
* @typedef {(number|string)} encodable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Yjs instance handles the state of shared data.
|
||||||
|
*
|
||||||
|
* @param {string} room Users in the same room share the same content
|
||||||
|
* @param {Object} opts Connector definition
|
||||||
|
* @param {AbstractPersistence} persistence Persistence adapter instance
|
||||||
|
*/
|
||||||
export default class Y extends NamedEventHandler {
|
export default class Y extends NamedEventHandler {
|
||||||
constructor (room, opts, persistence) {
|
constructor (room, opts, persistence) {
|
||||||
super()
|
super()
|
||||||
|
/**
|
||||||
|
* The room name that this Yjs instance connects to.
|
||||||
|
* @type {String}
|
||||||
|
*/
|
||||||
this.room = room
|
this.room = room
|
||||||
if (opts != null) {
|
if (opts != null) {
|
||||||
opts.connector.room = room
|
opts.connector.room = room
|
||||||
@ -37,6 +60,7 @@ export default class Y extends NamedEventHandler {
|
|||||||
this._contentReady = false
|
this._contentReady = false
|
||||||
this._opts = opts
|
this._opts = opts
|
||||||
this.userID = generateUserID()
|
this.userID = generateUserID()
|
||||||
|
// TODO: This should be a Map so we can use encodables as keys
|
||||||
this.share = {}
|
this.share = {}
|
||||||
this.ds = new DeleteStore(this)
|
this.ds = new DeleteStore(this)
|
||||||
this.os = new OperationStore(this)
|
this.os = new OperationStore(this)
|
||||||
@ -44,6 +68,10 @@ export default class Y extends NamedEventHandler {
|
|||||||
this._missingStructs = new Map()
|
this._missingStructs = new Map()
|
||||||
this._readyToIntegrate = []
|
this._readyToIntegrate = []
|
||||||
this._transaction = null
|
this._transaction = null
|
||||||
|
/**
|
||||||
|
* The {@link AbstractConnector}.that is used by this Yjs instance.
|
||||||
|
* @type {AbstractConnector}
|
||||||
|
*/
|
||||||
this.connector = null
|
this.connector = null
|
||||||
this.connected = false
|
this.connected = false
|
||||||
let initConnection = () => {
|
let initConnection = () => {
|
||||||
@ -53,11 +81,15 @@ export default class Y extends NamedEventHandler {
|
|||||||
this.emit('connectorReady')
|
this.emit('connectorReady')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* The {@link AbstractPersistence} that is used by this Yjs instance.
|
||||||
|
* @type {AbstractPersistence}
|
||||||
|
*/
|
||||||
|
this.persistence = null
|
||||||
if (persistence != null) {
|
if (persistence != null) {
|
||||||
this.persistence = persistence
|
this.persistence = persistence
|
||||||
persistence._init(this).then(initConnection)
|
persistence._init(this).then(initConnection)
|
||||||
} else {
|
} else {
|
||||||
this.persistence = null
|
|
||||||
initConnection()
|
initConnection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,6 +109,14 @@ export default class Y extends NamedEventHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_beforeChange () {}
|
_beforeChange () {}
|
||||||
|
/**
|
||||||
|
* Changes that happen inside of a transaction are bundled. This means that
|
||||||
|
* the observer fires _after_ the transaction is finished and that all changes
|
||||||
|
* that happened inside of the transaction are sent as one message to the
|
||||||
|
* other peers.
|
||||||
|
*
|
||||||
|
* @param {Function} f The function that should be executed as a transaction
|
||||||
|
*/
|
||||||
transact (f, remote = false) {
|
transact (f, remote = false) {
|
||||||
let initialCall = this._transaction === null
|
let initialCall = this._transaction === null
|
||||||
if (initialCall) {
|
if (initialCall) {
|
||||||
@ -117,13 +157,55 @@ export default class Y extends NamedEventHandler {
|
|||||||
this.emit('afterTransaction', this, transaction, remote)
|
this.emit('afterTransaction', this, transaction, remote)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// fake _start for root properties (y.set('name', type))
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Fake _start for root properties (y.set('name', type))
|
||||||
|
*/
|
||||||
get _start () {
|
get _start () {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* Fake _start for root properties (y.set('name', type))
|
||||||
|
*/
|
||||||
set _start (start) {
|
set _start (start) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define a shared data type.
|
||||||
|
*
|
||||||
|
* Multiple calls of `y.define(name, TypeConstructor)` yield the same result
|
||||||
|
* and do not overwrite each other. I.e.
|
||||||
|
* `y.define(name, type) === y.define(name, type)`
|
||||||
|
*
|
||||||
|
* After this method is called, the type is also available on `y.share[name]`.
|
||||||
|
*
|
||||||
|
* *Best Practices:*
|
||||||
|
* Either define all types right after the Yjs instance is created or always
|
||||||
|
* use `y.define(..)` when accessing a type.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Option 1
|
||||||
|
* const y = new Y(..)
|
||||||
|
* y.define('myArray', YArray)
|
||||||
|
* y.define('myMap', YMap)
|
||||||
|
* // .. when accessing the type use y.share[name]
|
||||||
|
* y.share.myArray.insert(..)
|
||||||
|
* y.share.myMap.set(..)
|
||||||
|
*
|
||||||
|
* // Option2
|
||||||
|
* const y = new Y(..)
|
||||||
|
* // .. when accessing the type use `y.define(..)`
|
||||||
|
* y.define('myArray', YArray).insert(..)
|
||||||
|
* y.define('myMap', YMap).set(..)
|
||||||
|
*
|
||||||
|
* @param {String} name
|
||||||
|
* @param {YType Constructor} TypeConstructor The constructor of the type definition
|
||||||
|
* @returns {YType} The created type
|
||||||
|
*/
|
||||||
define (name, TypeConstructor) {
|
define (name, TypeConstructor) {
|
||||||
let id = new RootID(name, TypeConstructor)
|
let id = new RootID(name, TypeConstructor)
|
||||||
let type = this.os.get(id)
|
let type = this.os.get(id)
|
||||||
@ -134,9 +216,23 @@ export default class Y extends NamedEventHandler {
|
|||||||
}
|
}
|
||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a defined type. The type must be defined locally. First define the
|
||||||
|
* type with {@link define}.
|
||||||
|
*
|
||||||
|
* This returns the same value as `y.share[name]`
|
||||||
|
*
|
||||||
|
* @param {String} name The typename
|
||||||
|
*/
|
||||||
get (name) {
|
get (name) {
|
||||||
return this.share[name]
|
return this.share[name]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect this Yjs Instance from the network. The connector will
|
||||||
|
* unsubscribe from the room and document updates are not shared anymore.
|
||||||
|
*/
|
||||||
disconnect () {
|
disconnect () {
|
||||||
if (this.connected) {
|
if (this.connected) {
|
||||||
this.connected = false
|
this.connected = false
|
||||||
@ -145,6 +241,10 @@ export default class Y extends NamedEventHandler {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If disconnected, tell the connector to reconnect to the room.
|
||||||
|
*/
|
||||||
reconnect () {
|
reconnect () {
|
||||||
if (!this.connected) {
|
if (!this.connected) {
|
||||||
this.connected = true
|
this.connected = true
|
||||||
@ -153,6 +253,11 @@ export default class Y extends NamedEventHandler {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect from the room, and destroy all traces of this Yjs instance.
|
||||||
|
* Persisted data will remain until removed by the persistence adapter.
|
||||||
|
*/
|
||||||
destroy () {
|
destroy () {
|
||||||
super.destroy()
|
super.destroy()
|
||||||
this.share = null
|
this.share = null
|
||||||
@ -171,7 +276,9 @@ export default class Y extends NamedEventHandler {
|
|||||||
this.ds = null
|
this.ds = null
|
||||||
this.ss = null
|
this.ss = null
|
||||||
}
|
}
|
||||||
|
|
||||||
whenSynced () {
|
whenSynced () {
|
||||||
|
// TODO: remove this method
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
this.once('synced', () => {
|
this.once('synced', () => {
|
||||||
resolve()
|
resolve()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user