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
|
||||
bower_components
|
||||
docs
|
||||
/y.*
|
||||
/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": {
|
||||
"test": "npm run lint",
|
||||
"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",
|
||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||
"postversion": "npm run dist",
|
||||
@ -64,7 +66,9 @@
|
||||
"rollup-regenerator-runtime": "^6.23.1",
|
||||
"rollup-watch": "^3.2.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": {
|
||||
"debug": "^2.6.8"
|
||||
|
@ -1,7 +1,13 @@
|
||||
import ID from '../Util/ID.js'
|
||||
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
|
||||
|
||||
/**
|
||||
* A BinaryDecoder handles the decoding of an ArrayBuffer
|
||||
*/
|
||||
export default class BinaryDecoder {
|
||||
/**
|
||||
* @param {Uint8Array|Buffer} buffer The binary data that this instance decodes
|
||||
*/
|
||||
constructor (buffer) {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
this.uint8arr = new Uint8Array(buffer)
|
||||
@ -12,6 +18,7 @@ export default class BinaryDecoder {
|
||||
}
|
||||
this.pos = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone this decoder instance
|
||||
* Optionally set a new position parameter
|
||||
@ -21,26 +28,32 @@ export default class BinaryDecoder {
|
||||
decoder.pos = newPos
|
||||
return decoder
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of bytes
|
||||
*/
|
||||
get length () {
|
||||
return this.uint8arr.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip one byte, jump to the next position
|
||||
*/
|
||||
skip8 () {
|
||||
this.pos++
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one byte as unsigned integer
|
||||
*/
|
||||
readUint8 () {
|
||||
return this.uint8arr[this.pos++]
|
||||
}
|
||||
|
||||
/**
|
||||
* Read 4 bytes as unsigned integer
|
||||
*
|
||||
* @return number An unsigned integer
|
||||
*/
|
||||
readUint32 () {
|
||||
let uint =
|
||||
@ -51,19 +64,24 @@ export default class BinaryDecoder {
|
||||
this.pos += 4
|
||||
return uint
|
||||
}
|
||||
|
||||
/**
|
||||
* Look ahead without incrementing position
|
||||
* to the next byte and read it as unsigned integer
|
||||
*
|
||||
* @return number An unsigned integer
|
||||
*/
|
||||
peekUint8 () {
|
||||
return this.uint8arr[this.pos]
|
||||
}
|
||||
|
||||
/**
|
||||
* Read unsigned integer (32bit) with variable length
|
||||
* 1/8th of the storage is used as encoding overhead
|
||||
* - numbers < 2^7 is stored in one byte
|
||||
* - numbers < 2^14 is stored in two bytes
|
||||
* ..
|
||||
* * numbers < 2^7 is stored in one byte
|
||||
* * numbers < 2^14 is stored in two bytes
|
||||
*
|
||||
* @return number An unsigned integer
|
||||
*/
|
||||
readVarUint () {
|
||||
let num = 0
|
||||
@ -80,9 +98,12 @@ export default class BinaryDecoder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 () {
|
||||
let len = this.readVarUint()
|
||||
@ -94,7 +115,7 @@ export default class BinaryDecoder {
|
||||
return decodeURIComponent(escape(encodedString))
|
||||
}
|
||||
/**
|
||||
* Look ahead and read varString without incrementing position
|
||||
* Look ahead and read varString without incrementing position
|
||||
*/
|
||||
peekVarString () {
|
||||
let pos = this.pos
|
||||
@ -104,8 +125,10 @@ export default class BinaryDecoder {
|
||||
}
|
||||
/**
|
||||
* Read ID
|
||||
* - If first varUint read is 0xFFFFFF a RootID is returned
|
||||
* - Otherwise an ID is returned
|
||||
* * If first varUint read is 0xFFFFFF a RootID is returned
|
||||
* * Otherwise an ID is returned
|
||||
*
|
||||
* @return ID
|
||||
*/
|
||||
readID () {
|
||||
let user = this.readVarUint()
|
||||
|
@ -3,41 +3,81 @@ import { RootFakeUserID } from '../Util/RootID.js'
|
||||
const bits7 = 0b1111111
|
||||
const bits8 = 0b11111111
|
||||
|
||||
/**
|
||||
* A BinaryEncoder handles the encoding to an ArrayBuffer
|
||||
*/
|
||||
export default class BinaryEncoder {
|
||||
constructor () {
|
||||
// TODO: implement chained Uint8Array buffers instead of Array buffer
|
||||
this.data = []
|
||||
}
|
||||
|
||||
/**
|
||||
* The current length of the encoded data
|
||||
*/
|
||||
get length () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
/**
|
||||
* The current write pointer (the same as {@link length}).
|
||||
*/
|
||||
get pos () {
|
||||
return this.data.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ArrayBuffer
|
||||
*
|
||||
* @return {Uint8Array}
|
||||
*/
|
||||
createBuffer () {
|
||||
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) {
|
||||
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) {
|
||||
this.data[pos] = num & bits8
|
||||
}
|
||||
|
||||
/**
|
||||
* Write two bytes as an unsigned integer
|
||||
*
|
||||
* @param {number} pos The number that is to be encoded
|
||||
*/
|
||||
writeUint16 (num) {
|
||||
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) {
|
||||
this.data[pos] = num & 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) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
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) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
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) {
|
||||
while (num >= 0b10000000) {
|
||||
this.data.push(0b10000000 | (bits7 & num))
|
||||
@ -60,6 +111,11 @@ export default class BinaryEncoder {
|
||||
this.data.push(bits7 & num)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a variable length string.
|
||||
*
|
||||
* @param {number} pos The number that is to be encoded
|
||||
*/
|
||||
writeVarString (str) {
|
||||
let encodedString = unescape(encodeURIComponent(str))
|
||||
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) {
|
||||
const user = id.user
|
||||
this.writeVarUint(user)
|
||||
|
@ -1,12 +1,34 @@
|
||||
|
||||
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 {
|
||||
/**
|
||||
* @param {YType} type Yjs type
|
||||
* @param {any} target Binding Target
|
||||
*/
|
||||
constructor (type, target) {
|
||||
this.type = type
|
||||
this.target = target
|
||||
this._mutualExclude = createMutualExclude()
|
||||
}
|
||||
/**
|
||||
* Remove all data observers (both from the type and th target).
|
||||
*/
|
||||
destroy () {
|
||||
this.type = null
|
||||
this.target = null
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import Binding from './Binding.js'
|
||||
|
||||
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 {
|
||||
constructor (textType, quillInstance) {
|
||||
// Binding handles textType as this.type and quillInstance as this.target
|
||||
super(textType, quillInstance)
|
||||
/**
|
||||
* @param {YText} textType
|
||||
* @param {Quill} quill
|
||||
*/
|
||||
constructor (textType, quill) {
|
||||
// Binding handles textType as this.type and quill as this.target
|
||||
super(textType, quill)
|
||||
// set initial value
|
||||
quillInstance.setContents(textType.toDelta(), 'yjs')
|
||||
quill.setContents(textType.toDelta(), 'yjs')
|
||||
// Observers are handled by this class
|
||||
this._typeObserver = typeObserver.bind(this)
|
||||
this._quillObserver = quillObserver.bind(this)
|
||||
textType.observe(this._typeObserver)
|
||||
quillInstance.on('text-change', this._quillObserver)
|
||||
quill.on('text-change', this._quillObserver)
|
||||
}
|
||||
destroy () {
|
||||
// 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 {
|
||||
constructor (textType, domTextarea) {
|
||||
// Binding handles textType as this.type and domTextarea as this.target
|
||||
|
@ -1,8 +1,15 @@
|
||||
|
||||
import { writeStructs } from './syncStep1.js'
|
||||
import { integrateRemoteStructs } from './integrateRemoteStructs.js'
|
||||
import { readDeleteSet, writeDeleteSet } from './deleteSet.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) {
|
||||
y.transact(function () {
|
||||
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) {
|
||||
let encoder = new BinaryEncoder()
|
||||
writeStructs(y, encoder, new Map())
|
||||
|
@ -11,6 +11,7 @@ class MissingEntry {
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Integrate remote struct
|
||||
* When a remote struct is integrated, other structs might be ready to ready to
|
||||
* integrate.
|
||||
|
@ -30,6 +30,11 @@ export function sendSyncStep1 (connector, syncUser) {
|
||||
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) {
|
||||
const lenPos = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
|
@ -13,6 +13,10 @@ function getFreshCnf () {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Abstract persistence class.
|
||||
*/
|
||||
export default class AbstractPersistence {
|
||||
constructor (opts) {
|
||||
this.opts = opts
|
||||
|
@ -3,6 +3,7 @@ import ID from '../Util/ID.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Delete all items in an ID-range
|
||||
* 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 {
|
||||
constructor () {
|
||||
this._target = 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) {
|
||||
// TODO: set target, and add it to missing if not found
|
||||
// There is an edge case in p2p networks!
|
||||
@ -54,15 +68,32 @@ export default class Delete {
|
||||
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) {
|
||||
encoder.writeUint8(getReference(this.constructor))
|
||||
encoder.writeID(this._targetID)
|
||||
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.
|
||||
* - 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)
|
||||
*/
|
||||
_integrate (y, locallyCreated = false) {
|
||||
@ -78,6 +109,12 @@ export default class Delete {
|
||||
y.persistence.saveStruct(y, this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Transform this Delete to a readable format.
|
||||
* Useful for logging as all Items implement this method.
|
||||
*/
|
||||
_logString () {
|
||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
||||
}
|
||||
|
@ -5,11 +5,12 @@ import Delete from './Delete.js'
|
||||
import { transactionTypeChanged } from '../Transaction.js'
|
||||
|
||||
/**
|
||||
* Helper utility to split an Item (see _splitAt)
|
||||
* - copy all properties from a to b
|
||||
* - connect a to b
|
||||
* @private
|
||||
* Helper utility to split an Item (see {@link Item#_splitAt})
|
||||
* - copies all properties from a to b
|
||||
* - connects a to b
|
||||
* - assigns the correct _id
|
||||
* - save b to os
|
||||
* - saves b to os
|
||||
*/
|
||||
export function splitHelper (y, a, b, diff) {
|
||||
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 {
|
||||
constructor () {
|
||||
this._id = null
|
||||
@ -58,14 +63,18 @@ export default class Item {
|
||||
this._deleted = false
|
||||
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 () {
|
||||
return new this.constructor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the effect of this operation.
|
||||
* @private
|
||||
* Redoes the effect of this operation.
|
||||
*/
|
||||
_redo (y) {
|
||||
if (this._redone !== null) {
|
||||
@ -106,27 +115,43 @@ export default class Item {
|
||||
return struct
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Computes the last content address of this Item.
|
||||
*/
|
||||
get _lastId () {
|
||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Computes the length of this Item.
|
||||
*/
|
||||
get _length () {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Some elements are not supposed to be addressable. For example, an
|
||||
* ItemFormat should not be retrievable via yarray.get(pos)
|
||||
* @private
|
||||
* 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 () {
|
||||
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
|
||||
* Returns right part after split
|
||||
* - diff === 0 => this
|
||||
* - diff === length => this._right
|
||||
* - otherwise => split _content and return right part of split
|
||||
* (see ItemJSON/ItemString for implementation)
|
||||
* * diff === 0 => this
|
||||
* * diff === length => this._right
|
||||
* * otherwise => split _content and return right part of split
|
||||
* (see {@link ItemJSON}/{@link ItemString} for implementation)
|
||||
*/
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
@ -134,6 +159,15 @@ export default class Item {
|
||||
}
|
||||
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) {
|
||||
if (!this._deleted) {
|
||||
this._deleted = true
|
||||
@ -152,17 +186,27 @@ export default class Item {
|
||||
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
|
||||
*/
|
||||
_beforeChange () {
|
||||
// nop
|
||||
}
|
||||
/*
|
||||
* - Integrate the struct so that other types/structs can see it
|
||||
* - Add this struct to y.os
|
||||
* - Check if this is struct deleted
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* * 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) {
|
||||
y._transaction.newTypes.add(this)
|
||||
@ -188,6 +232,7 @@ export default class Item {
|
||||
// or this types is new
|
||||
this._parent._beforeChange()
|
||||
}
|
||||
|
||||
/*
|
||||
# $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
|
||||
@ -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) {
|
||||
encoder.writeUint8(getReference(this.constructor))
|
||||
let info = 0
|
||||
@ -320,6 +375,16 @@ export default class Item {
|
||||
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) {
|
||||
let missing = []
|
||||
const info = decoder.readUint8()
|
||||
|
@ -30,6 +30,9 @@ export function getListItemIDByPosition (type, i) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract Yjs Type class
|
||||
*/
|
||||
export default class Type extends Item {
|
||||
constructor () {
|
||||
super()
|
||||
@ -39,6 +42,20 @@ export default class Type extends Item {
|
||||
this._eventHandler = 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) {
|
||||
if (type === this) {
|
||||
return []
|
||||
@ -65,6 +82,12 @@ export default class Type extends Item {
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Call event listeners with an event. This will also add an event to all
|
||||
* parents (for `.observeDeep` handlers).
|
||||
*/
|
||||
_callEventHandler (transaction, event) {
|
||||
const changedParentTypes = transaction.changedParentTypes
|
||||
this._eventHandler.callEventListeners(transaction, event)
|
||||
@ -79,6 +102,14 @@ export default class Type extends Item {
|
||||
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) {
|
||||
const y = this._y
|
||||
if (y !== null) {
|
||||
@ -87,18 +118,53 @@ export default class Type extends Item {
|
||||
f(y)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe all events that are created on this type.
|
||||
*
|
||||
* @param {Function} f Observer function
|
||||
*/
|
||||
observe (f) {
|
||||
this._eventHandler.addEventListener(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe all events that are created by this type and its children.
|
||||
*
|
||||
* @param {Function} f Observer function
|
||||
*/
|
||||
observeDeep (f) {
|
||||
this._deepEventHandler.addEventListener(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an observer function.
|
||||
*
|
||||
* @param {Function} f Observer function
|
||||
*/
|
||||
unobserve (f) {
|
||||
this._eventHandler.removeEventListener(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an observer function.
|
||||
*
|
||||
* @param {Function} f Observer function
|
||||
*/
|
||||
unobserveDeep (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) {
|
||||
super._integrate(y)
|
||||
this._y = y
|
||||
@ -117,6 +183,15 @@ export default class Type extends Item {
|
||||
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) {
|
||||
super._delete(y, createDelete)
|
||||
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 {
|
||||
constructor (y) {
|
||||
this.y = y
|
||||
|
@ -4,6 +4,13 @@ import ItemString from '../Struct/ItemString.js'
|
||||
import { logID } from '../MessageHandler/messageToString.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 {
|
||||
constructor (yarray, remote, transaction) {
|
||||
super(yarray)
|
||||
@ -12,6 +19,12 @@ export class YArrayEvent extends YEvent {
|
||||
this._addedElements = null
|
||||
this._removedElements = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Child elements that were added in this transaction.
|
||||
*
|
||||
* @return {Set}
|
||||
*/
|
||||
get addedElements () {
|
||||
if (this._addedElements === null) {
|
||||
const target = this.target
|
||||
@ -26,6 +39,12 @@ export class YArrayEvent extends YEvent {
|
||||
}
|
||||
return this._addedElements
|
||||
}
|
||||
|
||||
/**
|
||||
* Child elements that were removed in this transaction.
|
||||
*
|
||||
* @return {Set}
|
||||
*/
|
||||
get removedElements () {
|
||||
if (this._removedElements === null) {
|
||||
const target = this.target
|
||||
@ -42,29 +61,54 @@ export class YArrayEvent extends YEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A shared Array implementation.
|
||||
*/
|
||||
export default class YArray extends Type {
|
||||
/**
|
||||
* @private
|
||||
* Creates YArray Event and calls observers.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs, remote) {
|
||||
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
|
||||
while (n !== null) {
|
||||
if (!n._deleted && n._countable) {
|
||||
if (pos < n._length) {
|
||||
if (index < n._length) {
|
||||
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
||||
return n._content[pos]
|
||||
return n._content[index]
|
||||
} else {
|
||||
return n
|
||||
}
|
||||
}
|
||||
pos -= n._length
|
||||
index -= n._length
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this YArray to a JavaScript Array.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
toArray () {
|
||||
return this.map(c => c)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
toJSON () {
|
||||
return this.map(c => {
|
||||
if (c instanceof Type) {
|
||||
@ -77,6 +121,15 @@ export default class YArray extends Type {
|
||||
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) {
|
||||
const res = []
|
||||
this.forEach((c, i) => {
|
||||
@ -84,25 +137,35 @@ export default class YArray extends Type {
|
||||
})
|
||||
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) {
|
||||
let pos = 0
|
||||
let index = 0
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted && n._countable) {
|
||||
if (n instanceof Type) {
|
||||
f(n, pos++, this)
|
||||
f(n, index++, this)
|
||||
} else {
|
||||
const content = n._content
|
||||
const contentLen = content.length
|
||||
for (let i = 0; i < contentLen; i++) {
|
||||
pos++
|
||||
f(content[i], pos, this)
|
||||
index++
|
||||
f(content[i], index, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the length of this YArray.
|
||||
*/
|
||||
get length () {
|
||||
let length = 0
|
||||
let n = this._start
|
||||
@ -114,6 +177,7 @@ export default class YArray extends Type {
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return {
|
||||
next: function () {
|
||||
@ -143,14 +207,21 @@ export default class YArray extends Type {
|
||||
_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(() => {
|
||||
let item = this._start
|
||||
let count = 0
|
||||
while (item !== null && length > 0) {
|
||||
if (!item._deleted && item._countable) {
|
||||
if (count <= pos && pos < count + item._length) {
|
||||
const diffDel = pos - count
|
||||
if (count <= index && index < count + item._length) {
|
||||
const diffDel = index - count
|
||||
item = item._splitAt(this._y, diffDel)
|
||||
item._splitAt(this._y, length)
|
||||
length -= item._length
|
||||
@ -167,6 +238,14 @@ export default class YArray extends Type {
|
||||
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) {
|
||||
this._transact(y => {
|
||||
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(() => {
|
||||
let left = null
|
||||
let right = this._start
|
||||
@ -232,8 +328,8 @@ export default class YArray extends Type {
|
||||
const y = this._y
|
||||
while (right !== null) {
|
||||
const rightLen = right._deleted ? 0 : (right._length - 1)
|
||||
if (count <= pos && pos <= count + rightLen) {
|
||||
const splitDiff = pos - count
|
||||
if (count <= index && index <= count + rightLen) {
|
||||
const splitDiff = index - count
|
||||
right = right._splitAt(y, splitDiff)
|
||||
left = right._left
|
||||
count += splitDiff
|
||||
@ -245,12 +341,18 @@ export default class YArray extends Type {
|
||||
left = right
|
||||
right = right._right
|
||||
}
|
||||
if (pos > count) {
|
||||
throw new Error('Position exceeds array range!')
|
||||
if (index > count) {
|
||||
throw new Error('Index exceeds array range!')
|
||||
}
|
||||
this.insertAfter(left, content)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends content to this YArray.
|
||||
*
|
||||
* @param {Array} content Array of content to append.
|
||||
*/
|
||||
push (content) {
|
||||
let n = this._start
|
||||
let lastUndeleted = null
|
||||
@ -262,6 +364,12 @@ export default class YArray extends Type {
|
||||
}
|
||||
this.insertAfter(lastUndeleted, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Transform this YArray to a readable format.
|
||||
* Useful for logging as all Items implement this method.
|
||||
*/
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._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 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) {
|
||||
super(ymap)
|
||||
this.keysChanged = subs
|
||||
@ -12,10 +19,23 @@ class YMapEvent extends YEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A shared Map implementation.
|
||||
*/
|
||||
export default class YMap extends Type {
|
||||
/**
|
||||
* @private
|
||||
* Creates YMap Event and calls observers.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs, remote) {
|
||||
this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote))
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON () {
|
||||
const map = {}
|
||||
for (let [key, item] of this._map) {
|
||||
@ -35,7 +55,14 @@ export default class YMap extends Type {
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keys for each element in the YMap Type.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
keys () {
|
||||
// TODO: Should return either Iterator or Set!
|
||||
let keys = []
|
||||
for (let [key, value] of this._map) {
|
||||
if (!value._deleted) {
|
||||
@ -44,6 +71,12 @@ export default class YMap extends Type {
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specified element from this YMap.
|
||||
*
|
||||
* @param {encodable} key The key of the element to remove.
|
||||
*/
|
||||
delete (key) {
|
||||
this._transact((y) => {
|
||||
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) {
|
||||
this._transact(y => {
|
||||
const old = this._map.get(key) || 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
|
||||
// break here
|
||||
return value
|
||||
@ -87,6 +131,12 @@ export default class YMap extends Type {
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specified element from this YMap.
|
||||
*
|
||||
* @param {encodable} key The key of the element to return.
|
||||
*/
|
||||
get (key) {
|
||||
let v = this._map.get(key)
|
||||
if (v === undefined || v._deleted) {
|
||||
@ -98,6 +148,12 @@ export default class YMap extends Type {
|
||||
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) {
|
||||
let v = this._map.get(key)
|
||||
if (v === undefined || v._deleted) {
|
||||
@ -106,6 +162,12 @@ export default class YMap extends Type {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Transform this YMap to a readable format.
|
||||
* Useful for logging as all Items implement this method.
|
||||
*/
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._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]
|
||||
}
|
||||
|
||||
function findPosition (parent, pos) {
|
||||
function findPosition (parent, index) {
|
||||
let currentAttributes = new Map()
|
||||
let left = null
|
||||
let right = parent._start
|
||||
return findNextPosition(currentAttributes, parent, left, right, pos)
|
||||
return findNextPosition(currentAttributes, parent, left, right, index)
|
||||
}
|
||||
|
||||
// negate applied formats
|
||||
@ -212,11 +212,50 @@ function deleteText (y, length, parent, left, right, currentAttributes) {
|
||||
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 {
|
||||
constructor (ytext, remote, transaction) {
|
||||
super(ytext, remote, transaction)
|
||||
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 () {
|
||||
if (this._delta === null) {
|
||||
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 {
|
||||
constructor (string) {
|
||||
super()
|
||||
@ -388,9 +436,18 @@ export default class YText extends YArray {
|
||||
this._start = start
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Creates YMap Event and calls observers.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs, remote) {
|
||||
this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unformatted string representation of this YText type.
|
||||
*/
|
||||
toString () {
|
||||
let str = ''
|
||||
let n = this._start
|
||||
@ -402,6 +459,12 @@ export default class YText extends YArray {
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a {@link Delta} on this shared YText type.
|
||||
*
|
||||
* @param {Delta} delta The changes to apply on this element.
|
||||
*/
|
||||
applyDelta (delta) {
|
||||
this._transact(y => {
|
||||
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 () {
|
||||
let ops = []
|
||||
@ -461,42 +527,84 @@ export default class YText extends YArray {
|
||||
packStr()
|
||||
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) {
|
||||
return
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
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) {
|
||||
throw new Error('Embed must be an Object')
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
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) {
|
||||
return
|
||||
}
|
||||
this._transact(y => {
|
||||
let [left, right, currentAttributes] = findPosition(this, pos)
|
||||
let [left, right, currentAttributes] = findPosition(this, index)
|
||||
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 => {
|
||||
let [left, right, currentAttributes] = findPosition(this, pos)
|
||||
let [left, right, currentAttributes] = findPosition(this, index)
|
||||
if (right === null) {
|
||||
return
|
||||
}
|
||||
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 () {
|
||||
const left = this._left !== null ? this._left._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 { 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 {
|
||||
constructor (arg1, arg2, _document) {
|
||||
super()
|
||||
@ -20,11 +30,21 @@ export default class YXmlElement extends YXmlFragment {
|
||||
this._domFilter = arg2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Creates an Item with the same effect as this Item (without position effect)
|
||||
*/
|
||||
_copy () {
|
||||
let struct = super._copy()
|
||||
struct.nodeName = this.nodeName
|
||||
return struct
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Copies children and attributes from a dom node to this YXmlElement.
|
||||
*/
|
||||
_setDom (dom, _document) {
|
||||
if (this._dom != null) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
_document = _document || document
|
||||
this._dom = dom
|
||||
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) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.nodeName = decoder.readVarString()
|
||||
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) {
|
||||
super._toBinary(encoder)
|
||||
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) {
|
||||
if (this.nodeName === null) {
|
||||
throw new Error('nodeName must be defined!')
|
||||
@ -71,8 +132,9 @@ export default class YXmlElement extends YXmlFragment {
|
||||
}
|
||||
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
|
||||
* method to compare YXmlElements
|
||||
*/
|
||||
@ -93,18 +155,42 @@ export default class YXmlElement extends YXmlFragment {
|
||||
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
|
||||
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an attribute from this YXmlElement.
|
||||
*
|
||||
* @param {String} attributeName The attribute name that is to be removed.
|
||||
*/
|
||||
removeAttribute () {
|
||||
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 () {
|
||||
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 () {
|
||||
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 () {
|
||||
const obj = {}
|
||||
for (let [key, value] of this._map) {
|
||||
@ -114,6 +200,12 @@ export default class YXmlElement extends YXmlFragment {
|
||||
}
|
||||
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) {
|
||||
_document = _document || document
|
||||
let dom = this._dom
|
||||
|
@ -1,5 +1,8 @@
|
||||
import YEvent from '../../Util/YEvent.js'
|
||||
|
||||
/**
|
||||
* An Event that describes changes on a YXml Element or Yxml Fragment
|
||||
*/
|
||||
export default class YXmlEvent extends YEvent {
|
||||
constructor (target, subs, remote) {
|
||||
super(target)
|
||||
|
@ -36,6 +36,24 @@ function domToYXml (parent, doms, _document) {
|
||||
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 {
|
||||
constructor (root, f) {
|
||||
this._filter = f || (() => true)
|
||||
@ -46,6 +64,11 @@ class YXmlTreeWalker {
|
||||
[Symbol.iterator] () {
|
||||
return this
|
||||
}
|
||||
/**
|
||||
* Get the next node.
|
||||
*
|
||||
* @return {YXmlElement} The next node.
|
||||
*/
|
||||
next () {
|
||||
let n = this._currentNode
|
||||
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 {
|
||||
constructor () {
|
||||
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) {
|
||||
return new YXmlTreeWalker(this, filter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve first element that matches *query*
|
||||
* Similar to DOM's querySelector, but only accepts a subset of its queries
|
||||
* Returns the first YXmlElement that matches the query.
|
||||
* Similar to DOM's {@link querySelector}.
|
||||
*
|
||||
* Query support:
|
||||
* - tagname
|
||||
* TODO:
|
||||
* - id
|
||||
* - attribute
|
||||
*
|
||||
* @param {CSS_Selector} query The query on the children.
|
||||
* @return {?YXmlElement} The first element that matches the query or null.
|
||||
*/
|
||||
querySelector (query) {
|
||||
query = query.toUpperCase()
|
||||
@ -133,16 +174,52 @@ export default class YXmlFragment extends YArray {
|
||||
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) {
|
||||
query = query.toUpperCase()
|
||||
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) {
|
||||
this._scrollElement = scrollElement
|
||||
this.forEach(xml => {
|
||||
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) {
|
||||
this._domFilter = f
|
||||
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) {
|
||||
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 () {
|
||||
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) {
|
||||
this._unbindFromDom()
|
||||
super._delete(y, createDelete)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Unbind this YXmlFragment from the Dom.
|
||||
*/
|
||||
_unbindFromDom () {
|
||||
if (this._domObserver != null) {
|
||||
this._domObserver.disconnect()
|
||||
@ -191,19 +293,53 @@ export default class YXmlFragment extends YArray {
|
||||
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) {
|
||||
const types = domToYXml(this, doms, _document)
|
||||
this.insertAfter(prev, 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)
|
||||
this.insert(pos, types)
|
||||
this.insert(index, types)
|
||||
return types
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Dom representation of this YXml type..
|
||||
*/
|
||||
getDom () {
|
||||
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) {
|
||||
if (this._dom != null) {
|
||||
this._unbindFromDom()
|
||||
@ -217,8 +353,12 @@ export default class YXmlFragment extends YArray {
|
||||
})
|
||||
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) {
|
||||
_document = _document || document
|
||||
this._dom = dom
|
||||
@ -346,6 +486,12 @@ export default class YXmlFragment extends YArray {
|
||||
}
|
||||
return dom
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Transform this YXml Type to a readable format.
|
||||
* Useful for logging as all Items implement this method.
|
||||
*/
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
const origin = this._origin !== null ? this._origin._lastId : null
|
||||
|
@ -1,6 +1,11 @@
|
||||
import YMap from '../YMap.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 {
|
||||
constructor (hookName, dom) {
|
||||
super()
|
||||
@ -14,11 +19,20 @@ export default class YXmlHook extends YMap {
|
||||
getHook(hookName).fillType(dom, this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Creates an Item with the same effect as this Item (without position effect)
|
||||
*/
|
||||
_copy () {
|
||||
const struct = super._copy()
|
||||
struct.hookName = this.hookName
|
||||
return struct
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Dom representation of this YXmlHook.
|
||||
*/
|
||||
getDom (_document) {
|
||||
_document = _document || document
|
||||
if (this._dom === null) {
|
||||
@ -29,20 +43,56 @@ export default class YXmlHook extends YMap {
|
||||
}
|
||||
return this._dom
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Removes the Dom binding.
|
||||
*/
|
||||
_unbindFromDom () {
|
||||
this._dom._yxml = null
|
||||
this._yxml = null
|
||||
// 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) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.hookName = decoder.readVarString()
|
||||
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) {
|
||||
super._toBinary(encoder)
|
||||
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) {
|
||||
if (this.hookName === null) {
|
||||
throw new Error('hookName must be defined!')
|
||||
|
@ -1,5 +1,11 @@
|
||||
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 {
|
||||
constructor (arg1) {
|
||||
let dom = null
|
||||
@ -56,6 +62,15 @@ export default class YXmlText extends YText {
|
||||
enableSmartScrolling (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) {
|
||||
if (this._dom != null) {
|
||||
this._unbindFromDom()
|
||||
@ -67,6 +82,10 @@ export default class YXmlText extends YText {
|
||||
this._dom = dom
|
||||
dom._yxml = this
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Dom representation of this YXmlText.
|
||||
*/
|
||||
getDom (_document) {
|
||||
_document = _document || document
|
||||
if (this._dom === null) {
|
||||
@ -76,10 +95,24 @@ export default class YXmlText extends YText {
|
||||
}
|
||||
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) {
|
||||
this._unbindFromDom()
|
||||
super._delete(y, createDelete)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Unbind this YXmlText from the Dom.
|
||||
*/
|
||||
_unbindFromDom () {
|
||||
if (this._domObserver != null) {
|
||||
this._domObserver.disconnect()
|
||||
|
@ -184,8 +184,8 @@ export function reflectChangesOnDom (events, _document) {
|
||||
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.
|
||||
* - _yxmlHasAttributes
|
||||
* - _yxmlHasChildren
|
||||
|
@ -1,22 +1,56 @@
|
||||
|
||||
/**
|
||||
* General event handler implementation.
|
||||
*/
|
||||
export default class EventHandler {
|
||||
constructor () {
|
||||
this.eventListeners = []
|
||||
}
|
||||
|
||||
/**
|
||||
* To prevent memory leaks, call this method when the eventListeners won't be
|
||||
* used anymore.
|
||||
*/
|
||||
destroy () {
|
||||
this.eventListeners = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener that is called when
|
||||
* {@link EventHandler#callEventListeners} is called.
|
||||
*
|
||||
* @param {Function} f The event handler.
|
||||
*/
|
||||
addEventListener (f) {
|
||||
this.eventListeners.push(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an event listener.
|
||||
*
|
||||
* @param {Function} f The event handler that was added with
|
||||
* {@link EventHandler#addEventListener}
|
||||
*/
|
||||
removeEventListener (f) {
|
||||
this.eventListeners = this.eventListeners.filter(function (g) {
|
||||
return f !== g
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all event listeners.
|
||||
*/
|
||||
removeAllEventListeners () {
|
||||
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) {
|
||||
for (var i = 0; i < this.eventListeners.length; i++) {
|
||||
try {
|
||||
|
@ -1,8 +1,19 @@
|
||||
|
||||
/**
|
||||
* Handles named events.
|
||||
*/
|
||||
export default class NamedEventHandler {
|
||||
constructor () {
|
||||
this._eventListener = 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) {
|
||||
let listeners = this._eventListener.get(name)
|
||||
if (listeners === undefined) {
|
||||
@ -14,14 +25,34 @@ export default class NamedEventHandler {
|
||||
}
|
||||
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) {
|
||||
let listeners = this._getListener(name)
|
||||
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) {
|
||||
let listeners = this._getListener(name)
|
||||
listeners.on.add(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Init the saved state for an event name.
|
||||
*/
|
||||
_initStateListener (name) {
|
||||
let state = this._stateListener.get(name)
|
||||
if (state === undefined) {
|
||||
@ -33,9 +64,20 @@ export default class NamedEventHandler {
|
||||
}
|
||||
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) {
|
||||
return this._initStateListener(name).promise
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event listener that was registered with either
|
||||
* {@link EventHandler#on} or {@link EventHandler#once}.
|
||||
*/
|
||||
off (name, f) {
|
||||
if (name == null || f == null) {
|
||||
throw new Error('You must specify event name and function!')
|
||||
@ -46,6 +88,14 @@ export default class NamedEventHandler {
|
||||
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) {
|
||||
this._initStateListener(name).resolve()
|
||||
const listener = this._eventListener.get(name)
|
||||
|
@ -64,7 +64,15 @@ function applyReverseOperation (y, scope, reverseBuffer) {
|
||||
return performedUndo
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a history of locally applied operations. The UndoManager handles the
|
||||
* undoing and redoing of locally created changes.
|
||||
*/
|
||||
export default class UndoManager {
|
||||
/**
|
||||
* @param {YType} scope The scope on which to listen for changes.
|
||||
* @param {Object} options Optionally provided configuration.
|
||||
*/
|
||||
constructor (scope, options = {}) {
|
||||
this.options = options
|
||||
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
|
||||
@ -101,12 +109,20 @@ export default class UndoManager {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the last locally created change.
|
||||
*/
|
||||
undo () {
|
||||
this._undoing = true
|
||||
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer)
|
||||
this._undoing = false
|
||||
return performedUndo
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the last locally created change.
|
||||
*/
|
||||
redo () {
|
||||
this._redoing = true
|
||||
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer)
|
||||
|
@ -1,9 +1,27 @@
|
||||
|
||||
/**
|
||||
* YEvent describes the changes on a YType.
|
||||
*/
|
||||
export default class YEvent {
|
||||
/**
|
||||
* @param {YType} target The changed type.
|
||||
*/
|
||||
constructor (target) {
|
||||
this.target = 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 () {
|
||||
const path = []
|
||||
let type = this.target
|
||||
|
@ -1,7 +1,43 @@
|
||||
import ID from './ID.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) {
|
||||
// TODO: rename to createRelativePosition
|
||||
let t = type._start
|
||||
while (t !== null) {
|
||||
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]
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
if (rpos[0] === 'endof') {
|
||||
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'
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
constructor (room, opts, persistence) {
|
||||
super()
|
||||
/**
|
||||
* The room name that this Yjs instance connects to.
|
||||
* @type {String}
|
||||
*/
|
||||
this.room = room
|
||||
if (opts != null) {
|
||||
opts.connector.room = room
|
||||
@ -37,6 +60,7 @@ export default class Y extends NamedEventHandler {
|
||||
this._contentReady = false
|
||||
this._opts = opts
|
||||
this.userID = generateUserID()
|
||||
// TODO: This should be a Map so we can use encodables as keys
|
||||
this.share = {}
|
||||
this.ds = new DeleteStore(this)
|
||||
this.os = new OperationStore(this)
|
||||
@ -44,6 +68,10 @@ export default class Y extends NamedEventHandler {
|
||||
this._missingStructs = new Map()
|
||||
this._readyToIntegrate = []
|
||||
this._transaction = null
|
||||
/**
|
||||
* The {@link AbstractConnector}.that is used by this Yjs instance.
|
||||
* @type {AbstractConnector}
|
||||
*/
|
||||
this.connector = null
|
||||
this.connected = false
|
||||
let initConnection = () => {
|
||||
@ -53,11 +81,15 @@ export default class Y extends NamedEventHandler {
|
||||
this.emit('connectorReady')
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The {@link AbstractPersistence} that is used by this Yjs instance.
|
||||
* @type {AbstractPersistence}
|
||||
*/
|
||||
this.persistence = null
|
||||
if (persistence != null) {
|
||||
this.persistence = persistence
|
||||
persistence._init(this).then(initConnection)
|
||||
} else {
|
||||
this.persistence = null
|
||||
initConnection()
|
||||
}
|
||||
}
|
||||
@ -77,6 +109,14 @@ export default class Y extends NamedEventHandler {
|
||||
}
|
||||
}
|
||||
_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) {
|
||||
let initialCall = this._transaction === null
|
||||
if (initialCall) {
|
||||
@ -117,13 +157,55 @@ export default class Y extends NamedEventHandler {
|
||||
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 () {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Fake _start for root properties (y.set('name', type))
|
||||
*/
|
||||
set _start (start) {
|
||||
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) {
|
||||
let id = new RootID(name, TypeConstructor)
|
||||
let type = this.os.get(id)
|
||||
@ -134,9 +216,23 @@ export default class Y extends NamedEventHandler {
|
||||
}
|
||||
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) {
|
||||
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 () {
|
||||
if (this.connected) {
|
||||
this.connected = false
|
||||
@ -145,6 +241,10 @@ export default class Y extends NamedEventHandler {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If disconnected, tell the connector to reconnect to the room.
|
||||
*/
|
||||
reconnect () {
|
||||
if (!this.connected) {
|
||||
this.connected = true
|
||||
@ -153,6 +253,11 @@ export default class Y extends NamedEventHandler {
|
||||
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 () {
|
||||
super.destroy()
|
||||
this.share = null
|
||||
@ -171,7 +276,9 @@ export default class Y extends NamedEventHandler {
|
||||
this.ds = null
|
||||
this.ss = null
|
||||
}
|
||||
|
||||
whenSynced () {
|
||||
// TODO: remove this method
|
||||
return new Promise(resolve => {
|
||||
this.once('synced', () => {
|
||||
resolve()
|
||||
|
Loading…
x
Reference in New Issue
Block a user