big documentation update - all public functions and classes are documented now

This commit is contained in:
Kevin Jahns 2018-03-05 03:03:40 +01:00
parent dc22a79ac4
commit a9b610479d
33 changed files with 2426 additions and 168 deletions

10
.esdoc.json Normal file
View File

@ -0,0 +1,10 @@
{
"source": "./src",
"destination": "./docs",
"plugins": [{
"name": "esdoc-standard-plugin",
"option": {
"accessor": {"access": ["public"], "autoPrivate": true}
}
}]
}

1
.gitignore vendored
View File

@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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}`
} }

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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!')

View File

@ -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()

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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
View File

@ -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()