fix y-text

This commit is contained in:
Kevin Jahns 2019-03-30 01:08:09 +01:00
parent c188f813a4
commit 1bc1e88d6a
5 changed files with 351 additions and 192 deletions

View File

@ -232,8 +232,12 @@ export class AbstractItem extends AbstractStruct {
} }
transaction.added.add(this) transaction.added.add(this)
// @ts-ignore // @ts-ignore
if (parent._item.deleted) { if (parent._item.deleted || (left !== null && parentSub !== null)) {
this.delete(transaction, false, true) // delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction)
} else if (parentSub !== null && left === null && right !== null) {
// this is the current attribute value of parent. delete right
right.delete(transaction)
} }
} }
@ -391,13 +395,10 @@ export class AbstractItem extends AbstractStruct {
* Mark this Item as deleted. * Mark this Item as deleted.
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {boolean} createDelete Whether to propagate a message that this
* Type was deleted.
* @param {boolean} [gcChildren]
* *
* @private * @private
*/ */
delete (transaction, createDelete = true, gcChildren) { delete (transaction) {
if (!this.deleted) { if (!this.deleted) {
const parent = this.parent const parent = this.parent
// adjust the length of parent // adjust the length of parent

View File

@ -16,28 +16,6 @@ import { ItemBinary } from '../structs/ItemBinary.js'
import { ID, createID } from '../utils/ID.js' // eslint-disable-line import { ID, createID } from '../utils/ID.js' // eslint-disable-line
import { getItemCleanStart } from '../utils/StructStore.js' import { getItemCleanStart } from '../utils/StructStore.js'
/**
* Restructure children as if they were inserted one after another
* @param {Transaction} transaction
* @param {AbstractItem} start
*/
const integrateChildren = (transaction, start) => {
let right
while (true) {
right = start.right
start.id = nextID(transaction)
start.right = null
start.rightOrigin = null
start.origin = start.left
start.integrate(transaction)
if (right !== null) {
start = right
} else {
break
}
}
}
/** /**
* Abstract Yjs Type class * Abstract Yjs Type class
*/ */
@ -81,21 +59,6 @@ export class AbstractType {
_integrate (transaction, item) { _integrate (transaction, item) {
this._y = transaction.y this._y = transaction.y
this._item = item this._item = item
// when integrating children we must make sure to
// integrate start
const start = this._start
if (start !== null) {
this._start = null
integrateChildren(transaction, start)
}
// integrate map children_integrate
const map = this._map
this._map = new Map()
map.forEach(t => {
t.right = null
t.rightOrigin = null
integrateChildren(transaction, t)
})
} }
/** /**
@ -336,6 +299,7 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
let jsonContent = [] let jsonContent = []
content.forEach(c => { content.forEach(c => {
switch (c.constructor) { switch (c.constructor) {
case Number:
case Object: case Object:
case Array: case Array:
case String: case String:
@ -398,3 +362,64 @@ export const typeMapDelete = (transaction, parent, key) => {
c.delete(transaction) c.delete(transaction)
} }
} }
/**
* @param {Transaction} transaction
* @param {AbstractType} parent
* @param {string} key
* @param {Object|number|Array<any>|string|ArrayBuffer|AbstractType} value
*/
export const typeMapSet = (transaction, parent, key, value) => {
const right = parent._map.get(key) || null
switch (value.constructor) {
case Number:
case Object:
case Array:
case String:
new ItemJSON(nextID(transaction), null, right, parent, key, [value]).integrate(transaction)
break
case ArrayBuffer:
new ItemBinary(nextID(transaction), null, right, parent, key, value).integrate(transaction)
break
default:
if (value instanceof AbstractType) {
new ItemType(nextID(transaction), null, right, parent, key, value).integrate(transaction)
} else {
throw new Error('Unexpected content type')
}
}
}
/**
* @param {AbstractType} parent
* @param {string} key
* @return {Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType|undefined}
*/
export const typeMapGet = (parent, key) => {
const val = parent._map.get(key)
return val !== undefined && !val.deleted ? val.getContent()[0] : undefined
}
/**
* @param {AbstractType} parent
* @param {string} key
* @return {boolean}
*/
export const typeMapHas = (parent, key) => {
const val = parent._map.get(key)
return val !== undefined && !val.deleted
}
/**
* @param {AbstractType} parent
* @param {string} key
* @param {Snapshot} snapshot
* @return {Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType|undefined}
*/
export const typeMapGetSnapshot = (parent, key, snapshot) => {
let v = parent._map.get(key) || null
while (v !== null && (!snapshot.sm.has(v.id.client) || v.id.clock >= (snapshot.sm.get(v.id.client) || 0))) {
v = v.right
}
return v !== null && isVisible(v, snapshot) ? v.getContent()[0] : undefined
}

View File

@ -13,12 +13,10 @@ import * as decoding from 'lib0/decoding.js' // eslint-disable-line
/** /**
* Event that describes the changes on a YArray * Event that describes the changes on a YArray
*
* @template T
*/ */
export class YArrayEvent extends YEvent { export class YArrayEvent extends YEvent {
/** /**
* @param {YArray<T>} yarray The changed type * @param {AbstractType} yarray The changed type
* @param {Transaction} transaction The transaction object * @param {Transaction} transaction The transaction object
*/ */
constructor (yarray, transaction) { constructor (yarray, transaction) {

View File

@ -5,82 +5,109 @@
import { ItemEmbed } from '../structs/ItemEmbed.js' import { ItemEmbed } from '../structs/ItemEmbed.js'
import { ItemString } from '../structs/ItemString.js' import { ItemString } from '../structs/ItemString.js'
import { ItemFormat } from '../structs/ItemFormat.js' import { ItemFormat } from '../structs/ItemFormat.js'
import { YArrayEvent, YArray } from './YArray.js' import { YArrayEvent } from './YArray.js'
import { isVisible } from '../utils/Snapshot.js' import { ItemType } from '../structs/ItemType.js' // eslint-disable-line
import { AbstractType } from './AbstractType.js'
import { AbstractItem } from '../structs/AbstractItem.js' // eslint-disable-line
import { isVisible, Snapshot } from '../utils/Snapshot.js' // eslint-disable-line
import { getItemCleanStart, StructStore } from '../utils/StructStore.js' // eslint-disable-line
import { Transaction, nextID } from '../utils/Transaction.js' // eslint-disable-line
import { createID } from '../utils/ID.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
/** /**
* @private * @private
* @param {Transaction} transaction
* @param {StructStore} store
* @param {Map<string,any>} currentAttributes
* @param {AbstractItem|null} left
* @param {AbstractItem|null} right
* @param {number} count
* @return {{left:AbstractItem|null,right:AbstractItem|null,currentAttributes:Map<string,any>}}
*/ */
const findNextPosition = (currentAttributes, parent, left, right, count) => { const findNextPosition = (transaction, store, currentAttributes, left, right, count) => {
while (right !== null && count > 0) { while (right !== null && count > 0) {
switch (right.constructor) { switch (right.constructor) {
case ItemEmbed: case ItemEmbed:
case ItemString: case ItemString:
const rightLen = right._deleted ? 0 : (right._length - 1) if (!right.deleted) {
if (count <= rightLen) { if (count < right.length) {
right = right._splitAt(parent._y, count) right = getItemCleanStart(store, transaction, createID(right.id.client, right.id.clock + count))
left = right._left left = right.left
return [left, right, currentAttributes] count = 0
} else {
count -= right.length
} }
if (right._deleted === false) {
count -= right._length
} }
break break
case ItemFormat: case ItemFormat:
if (right._deleted === false) { if (!right.deleted) {
// @ts-ignore right is ItemFormat
updateCurrentAttributes(currentAttributes, right) updateCurrentAttributes(currentAttributes, right)
} }
break break
} }
left = right left = right
right = right._right right = right.right
} }
return [left, right, currentAttributes] return { left, right, currentAttributes }
} }
/** /**
* @private * @private
* @param {Transaction} transaction
* @param {StructStore} store
* @param {AbstractType} parent
* @param {number} index
* @return {{left:AbstractItem|null,right:AbstractItem|null,currentAttributes:Map<string,any>}}
*/ */
const findPosition = (parent, index) => { const findPosition = (transaction, store, 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, index) return findNextPosition(transaction, store, currentAttributes, left, right, index)
} }
/** /**
* Negate applied formats * Negate applied formats
* *
* @private * @private
* @param {Transaction} transaction
* @param {AbstractType} parent
* @param {AbstractItem|null} left
* @param {AbstractItem|null} right
* @param {Map<string,any>} negatedAttributes
* @return {{left:AbstractItem|null,right:AbstractItem|null}}
*/ */
const insertNegatedAttributes = (y, parent, left, right, negatedAttributes) => { const insertNegatedAttributes = (transaction, parent, left, right, negatedAttributes) => {
// check if we really need to remove attributes // check if we really need to remove attributes
while ( while (
right !== null && ( right !== null && (
right._deleted === true || ( right.deleted === true || (
right.constructor === ItemFormat && right.constructor === ItemFormat &&
// @ts-ignore right is ItemFormat
(negatedAttributes.get(right.key) === right.value) (negatedAttributes.get(right.key) === right.value)
) )
) )
) { ) {
if (right._deleted === false) { if (!right.deleted) {
// @ts-ignore right is ItemFormat
negatedAttributes.delete(right.key) negatedAttributes.delete(right.key)
} }
left = right left = right
right = right._right right = right.right
} }
for (let [key, val] of negatedAttributes) { for (let [key, val] of negatedAttributes) {
let format = new ItemFormat() left = new ItemFormat(nextID(transaction), left, right, parent, null, key, val)
format.key = key left.integrate(transaction)
format.value = val
integrateItem(format, parent, y, left, right)
left = format
} }
return [left, right] return {left, right}
} }
/** /**
* @private * @private
* @param {Map<string,any>} currentAttributes
* @param {ItemFormat} item
*/ */
const updateCurrentAttributes = (currentAttributes, item) => { const updateCurrentAttributes = (currentAttributes, item) => {
const value = item.value const value = item.value
@ -94,30 +121,44 @@ const updateCurrentAttributes = (currentAttributes, item) => {
/** /**
* @private * @private
* @param {AbstractItem|null} left
* @param {AbstractItem|null} right
* @param {Map<string,any>} currentAttributes
* @param {Object<string,any>} attributes
* @return {{left:AbstractItem|null,right:AbstractItem|null}}
*/ */
const minimizeAttributeChanges = (left, right, currentAttributes, attributes) => { const minimizeAttributeChanges = (left, right, currentAttributes, attributes) => {
// go right while attributes[right.key] === right.value (or right is deleted) // go right while attributes[right.key] === right.value (or right is deleted)
while (true) { while (true) {
if (right === null) { if (right === null) {
break break
} else if (right._deleted === true) { } else if (right.deleted) {
// continue // continue
// @ts-ignore right is ItemFormat
} else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) { } else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) {
// found a format, update currentAttributes and continue // found a format, update currentAttributes and continue
// @ts-ignore right is ItemFormat
updateCurrentAttributes(currentAttributes, right) updateCurrentAttributes(currentAttributes, right)
} else { } else {
break break
} }
left = right left = right
right = right._right right = right.right
} }
return [left, right] return { left, right }
} }
/** /**
* @private * @private
*/ * @param {Transaction} transaction
const insertAttributes = (y, parent, left, right, attributes, currentAttributes) => { * @param {AbstractType} parent
* @param {AbstractItem|null} left
* @param {AbstractItem|null} right
* @param {Map<string,any>} currentAttributes
* @param {Object<string,any>} attributes
* @return {{left:AbstractItem|null,right:AbstractItem|null,negatedAttributes:Map<string,any>}}
**/
const insertAttributes = (transaction, parent, left, right, currentAttributes, attributes) => {
const negatedAttributes = new Map() const negatedAttributes = new Map()
// insert format-start items // insert format-start items
for (let key in attributes) { for (let key in attributes) {
@ -126,101 +167,128 @@ const insertAttributes = (y, parent, left, right, attributes, currentAttributes)
if (currentVal !== val) { if (currentVal !== val) {
// save negated attribute (set null if currentVal undefined) // save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal || null) negatedAttributes.set(key, currentVal || null)
let format = new ItemFormat() left = new ItemFormat(nextID(transaction), left, right, parent, null, key, val)
format.key = key left.integrate(transaction)
format.value = val
integrateItem(format, parent, y, left, right)
left = format
} }
} }
return [left, right, negatedAttributes] return { left, right, negatedAttributes }
} }
/** /**
* @private * @private
*/ * @param {Transaction} transaction
const insertText = (y, text, parent, left, right, currentAttributes, attributes) => { * @param {AbstractType} parent
* @param {AbstractItem|null} left
* @param {AbstractItem|null} right
* @param {Map<string,any>} currentAttributes
* @param {string} text
* @param {Object<string,any>} attributes
* @return {{left:AbstractItem|null,right:AbstractItem|null}}
**/
const insertText = (transaction, parent, left, right, currentAttributes, text, attributes) => {
for (let [key] of currentAttributes) { for (let [key] of currentAttributes) {
if (attributes[key] === undefined) { if (attributes[key] === undefined) {
attributes[key] = null attributes[key] = null
} }
} }
[left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes) const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
let negatedAttributes const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
[left, right, negatedAttributes] = insertAttributes(y, parent, left, right, attributes, currentAttributes)
// insert content // insert content
let item
if (text.constructor === String) { if (text.constructor === String) {
item = new ItemString() left = new ItemString(nextID(transaction), insertPos.left, insertPos.right, parent, null, text)
item._content = text
} else { } else {
item = new ItemEmbed() left = new ItemEmbed(nextID(transaction), insertPos.left, insertPos.right, parent, null, text)
item.embed = text
} }
integrateItem(item, parent, y, left, right) left.integrate(transaction)
left = item return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
return insertNegatedAttributes(y, parent, left, right, negatedAttributes)
} }
/** /**
* @private * @private
* @param {Transaction} transaction
* @param {AbstractType} parent
* @param {AbstractItem|null} left
* @param {AbstractItem|null} right
* @param {Map<string,any>} currentAttributes
* @param {number} length
* @param {Object<string,any>} attributes
* @return {{left:AbstractItem|null,right:AbstractItem|null}}
*/ */
const formatText = (y, length, parent, left, right, currentAttributes, attributes) => { const formatText = (transaction, parent, left, right, currentAttributes, length, attributes) => {
[left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes) const minPos = minimizeAttributeChanges(left, right, currentAttributes, attributes)
let negatedAttributes const insertPos = insertAttributes(transaction, parent, minPos.left, minPos.right, currentAttributes, attributes)
[left, right, negatedAttributes] = insertAttributes(y, parent, left, right, attributes, currentAttributes) const negatedAttributes = insertPos.negatedAttributes
left = insertPos.left
right = insertPos.right
// iterate until first non-format or null is found // iterate until first non-format or null is found
// delete all formats with attributes[format.key] != null // delete all formats with attributes[format.key] != null
while (length > 0 && right !== null) { while (length > 0 && right !== null) {
if (right._deleted === false) { if (right.deleted === false) {
switch (right.constructor) { switch (right.constructor) {
case ItemFormat: case ItemFormat:
// @ts-ignore right is ItemFormat
const attr = attributes[right.key] const attr = attributes[right.key]
if (attr !== undefined) { if (attr !== undefined) {
// @ts-ignore right is ItemFormat
if (attr === right.value) { if (attr === right.value) {
// @ts-ignore right is ItemFormat
negatedAttributes.delete(right.key) negatedAttributes.delete(right.key)
} else { } else {
// @ts-ignore right is ItemFormat
negatedAttributes.set(right.key, right.value) negatedAttributes.set(right.key, right.value)
} }
right._delete(y) right.delete(transaction)
} }
// @ts-ignore right is ItemFormat
updateCurrentAttributes(currentAttributes, right) updateCurrentAttributes(currentAttributes, right)
break break
case ItemEmbed: case ItemEmbed:
case ItemString: case ItemString:
right._splitAt(y, length) if (length < right.length) {
length -= right._length getItemCleanStart(transaction.y.store, transaction, createID(right.id.client, right.id.clock + length))
}
length -= right.length
break break
} }
} }
left = right left = right
right = right._right right = right.right
} }
return insertNegatedAttributes(y, parent, left, right, negatedAttributes) return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes)
} }
/** /**
* @private * @private
* @param {Transaction} transaction
* @param {AbstractType} parent
* @param {AbstractItem|null} left
* @param {AbstractItem|null} right
* @param {Map<string,any>} currentAttributes
* @param {number} length
* @return {{left:AbstractItem|null,right:AbstractItem|null}}
*/ */
const deleteText = (y, length, parent, left, right, currentAttributes) => { const deleteText = (transaction, parent, left, right, currentAttributes, length) => {
while (length > 0 && right !== null) { while (length > 0 && right !== null) {
if (right._deleted === false) { if (right.deleted === false) {
switch (right.constructor) { switch (right.constructor) {
case ItemFormat: case ItemFormat:
// @ts-ignore right is ItemFormat
updateCurrentAttributes(currentAttributes, right) updateCurrentAttributes(currentAttributes, right)
break break
case ItemEmbed: case ItemEmbed:
case ItemString: case ItemString:
right._splitAt(y, length) if (length < right.length) {
length -= right._length getItemCleanStart(transaction.y.store, transaction, createID(right.id.client, right.id.clock + length))
right._delete(y) }
length -= right.length
right.delete(transaction)
break break
} }
} }
left = right left = right
right = right._right right = right.right
} }
return [left, right] return { left, right }
} }
// TODO: In the quill delta representation we should also use the format {ops:[..]} // TODO: In the quill delta representation we should also use the format {ops:[..]}
@ -237,7 +305,6 @@ const deleteText = (y, length, parent, left, right, currentAttributes) => {
* ] * ]
* } * }
* *
* @typedef {Array<Object>} Delta
*/ */
/** /**
@ -258,8 +325,15 @@ const deleteText = (y, length, parent, left, right, currentAttributes) => {
* @private * @private
*/ */
class YTextEvent extends YArrayEvent { class YTextEvent extends YArrayEvent {
constructor (ytext, remote, transaction) { /**
super(ytext, remote, transaction) * @param {AbstractType} ytext
* @param {Transaction} transaction
*/
constructor (ytext, transaction) {
super(ytext, transaction)
/**
* @type {Array<{delete:number|undefined,retain:number|undefined,insert:string|undefined,attributes:Object<string,any>}>|null}
*/
this._delta = null this._delta = null
} }
// TODO: Should put this in a separate function. toDelta shouldn't be included // TODO: Should put this in a separate function. toDelta shouldn't be included
@ -267,7 +341,7 @@ class YTextEvent extends YArrayEvent {
/** /**
* Compute the changes in the delta format. * Compute the changes in the delta format.
* *
* @return {Delta} A {@link https://quilljs.com/docs/delta/|Quill Delta}) that * @type {Array<{delete:number|undefined,retain:number|undefined,insert:string|undefined,attributes:Object<string,any>}>} A {@link https://quilljs.com/docs/delta/|Quill Delta}) that
* represents the changes on the document. * represents the changes on the document.
* *
* @public * @public
@ -275,20 +349,30 @@ class YTextEvent extends YArrayEvent {
get delta () { get delta () {
if (this._delta === null) { if (this._delta === null) {
const y = this.target._y const y = this.target._y
y.transact(() => { // @ts-ignore
let item = this.target._start y.transact(transaction => {
/**
* @type {Array<{delete:number|undefined,retain:number|undefined,insert:string|undefined,attributes:Object<string,any>}>}
*/
const delta = [] const delta = []
const added = this.addedElements const added = this.addedElements
const removed = this.removedElements const removed = this.removedElements
this._delta = delta
let action = null
let attributes = {} // counts added or removed new attributes for retain
const currentAttributes = new Map() // saves all current attributes for insert const currentAttributes = new Map() // saves all current attributes for insert
const oldAttributes = new Map() const oldAttributes = new Map()
let item = this.target._start
/**
* @type {string?}
*/
let action = null
/**
* @type {Object<string,any>}
*/
let attributes = {} // counts added or removed new attributes for retain
let insert = '' let insert = ''
let retain = 0 let retain = 0
let deleteLen = 0 let deleteLen = 0
const addOp = function addOp () { this._delta = delta
const addOp = () => {
if (action !== null) { if (action !== null) {
/** /**
* @type {any} * @type {any}
@ -332,6 +416,7 @@ class YTextEvent extends YArrayEvent {
if (added.has(item)) { if (added.has(item)) {
addOp() addOp()
action = 'insert' action = 'insert'
// @ts-ignore item is ItemFormat
insert = item.embed insert = item.embed
addOp() addOp()
} else if (removed.has(item)) { } else if (removed.has(item)) {
@ -340,7 +425,7 @@ class YTextEvent extends YArrayEvent {
action = 'delete' action = 'delete'
} }
deleteLen += 1 deleteLen += 1
} else if (item._deleted === false) { } else if (item.deleted === false) {
if (action !== 'retain') { if (action !== 'retain') {
addOp() addOp()
action = 'retain' action = 'retain'
@ -354,72 +439,89 @@ class YTextEvent extends YArrayEvent {
addOp() addOp()
action = 'insert' action = 'insert'
} }
insert += item._content // @ts-ignore
insert += item.string
} else if (removed.has(item)) { } else if (removed.has(item)) {
if (action !== 'delete') { if (action !== 'delete') {
addOp() addOp()
action = 'delete' action = 'delete'
} }
deleteLen += item._length deleteLen += item.length
} else if (item._deleted === false) { } else if (item.deleted === false) {
if (action !== 'retain') { if (action !== 'retain') {
addOp() addOp()
action = 'retain' action = 'retain'
} }
retain += item._length retain += item.length
} }
break break
case ItemFormat: case ItemFormat:
if (added.has(item)) { if (added.has(item)) {
// @ts-ignore item is ItemFormat
const curVal = currentAttributes.get(item.key) || null const curVal = currentAttributes.get(item.key) || null
// @ts-ignore item is ItemFormat
if (curVal !== item.value) { if (curVal !== item.value) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
// @ts-ignore item is ItemFormat
if (item.value === (oldAttributes.get(item.key) || null)) { if (item.value === (oldAttributes.get(item.key) || null)) {
// @ts-ignore item is ItemFormat
delete attributes[item.key] delete attributes[item.key]
} else { } else {
// @ts-ignore item is ItemFormat
attributes[item.key] = item.value attributes[item.key] = item.value
} }
} else { } else {
item._delete(y) item.delete(transaction)
} }
} else if (removed.has(item)) { } else if (removed.has(item)) {
// @ts-ignore item is ItemFormat
oldAttributes.set(item.key, item.value) oldAttributes.set(item.key, item.value)
// @ts-ignore item is ItemFormat
const curVal = currentAttributes.get(item.key) || null const curVal = currentAttributes.get(item.key) || null
// @ts-ignore item is ItemFormat
if (curVal !== item.value) { if (curVal !== item.value) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
// @ts-ignore item is ItemFormat
attributes[item.key] = curVal attributes[item.key] = curVal
} }
} else if (item._deleted === false) { } else if (item.deleted === false) {
// @ts-ignore item is ItemFormat
oldAttributes.set(item.key, item.value) oldAttributes.set(item.key, item.value)
// @ts-ignore item is ItemFormat
const attr = attributes[item.key] const attr = attributes[item.key]
if (attr !== undefined) { if (attr !== undefined) {
// @ts-ignore item is ItemFormat
if (attr !== item.value) { if (attr !== item.value) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
// @ts-ignore item is ItemFormat
if (item.value === null) { if (item.value === null) {
// @ts-ignore item is ItemFormat
attributes[item.key] = item.value attributes[item.key] = item.value
} else { } else {
// @ts-ignore item is ItemFormat
delete attributes[item.key] delete attributes[item.key]
} }
} else { } else {
item._delete(y) item.delete(transaction)
} }
} }
} }
if (item._deleted === false) { if (item.deleted === false) {
if (action === 'insert') { if (action === 'insert') {
addOp() addOp()
} }
// @ts-ignore item is ItemFormat
updateCurrentAttributes(currentAttributes, item) updateCurrentAttributes(currentAttributes, item)
} }
break break
} }
item = item._right item = item.right
} }
addOp() addOp()
while (this._delta.length > 0) { while (this._delta.length > 0) {
@ -433,6 +535,7 @@ class YTextEvent extends YArrayEvent {
} }
}) })
} }
// @ts-ignore _delta is defined above
return this._delta return this._delta
} }
} }
@ -444,18 +547,31 @@ class YTextEvent extends YArrayEvent {
* block formats (format information on a paragraph), embeds (complex elements * block formats (format information on a paragraph), embeds (complex elements
* like pictures and videos), and text formats (**bold**, *italic*). * like pictures and videos), and text formats (**bold**, *italic*).
*/ */
export class YText extends YArray { export class YText extends AbstractType {
/** /**
* @param {String} [string] The initial value of the YText. * @param {String} [string] The initial value of the YText.
*/ */
constructor (string) { constructor (string) {
super() super()
if (typeof string === 'string') { /**
const start = new ItemString() * @type {Array<string>?}
start._parent = this */
start._content = string this._prelimContent = string !== undefined ? [string] : []
this._start = start
} }
get length () {
return this._length
}
/**
* @param {Transaction} transaction
* @param {ItemType} item
*/
_integrate (transaction, item) {
super._integrate(transaction, item)
// @ts-ignore this._prelimContent is still defined
this.insert(0, this._prelimContent.join(''))
this._prelimContent = null
} }
/** /**
@ -494,6 +610,7 @@ export class YText extends YArray {
} }
toDomString () { toDomString () {
// @ts-ignore
return this.toDelta().map(delta => { return this.toDelta().map(delta => {
const nestedNodes = [] const nestedNodes = []
for (let nodeName in delta.attributes) { for (let nodeName in delta.attributes) {
@ -529,40 +646,47 @@ export class YText extends YArray {
/** /**
* Apply a {@link Delta} on this shared YText type. * Apply a {@link Delta} on this shared YText type.
* *
* @param {Delta} delta The changes to apply on this element. * @param {any} delta The changes to apply on this element.
* *
* @public * @public
*/ */
applyDelta (delta) { applyDelta (delta) {
this._transact(y => { if (this._y !== null) {
let left = null this._y.transact(transaction => {
let right = this._start /**
* @type {{left:AbstractItem|null,right:AbstractItem|null}}
*/
let pos = { left: null, right: this._start }
const currentAttributes = new Map() const currentAttributes = new Map()
for (let i = 0; i < delta.length; i++) { for (let i = 0; i < delta.length; i++) {
let op = delta[i] const op = delta[i]
if (op.insert !== undefined) { if (op.insert !== undefined) {
;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {}) pos = insertText(transaction, this, pos.left, pos.right, currentAttributes, op.insert, op.attributes || {})
} else if (op.retain !== undefined) { } else if (op.retain !== undefined) {
;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {}) pos = formatText(transaction, this, pos.left, pos.right, currentAttributes, op.retain, op.attributes || {})
} else if (op.delete !== undefined) { } else if (op.delete !== undefined) {
;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes) pos = deleteText(transaction, this, pos.left, pos.right, currentAttributes, op.delete)
} }
} }
}) })
} }
}
/** /**
* Returns the Delta representation of this YText type. * Returns the Delta representation of this YText type.
* *
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot] * @param {Snapshot} [snapshot]
* @param {import('../protocols/history.js').HistorySnapshot} [prevSnapshot] * @param {Snapshot} [prevSnapshot]
* @return {Delta} The Delta representation of this type. * @return {any} The Delta representation of this type.
* *
* @public * @public
*/ */
toDelta (snapshot, prevSnapshot) { toDelta (snapshot, prevSnapshot) {
let ops = [] /**
let currentAttributes = new Map() * @type{Array<any>}
*/
const ops = []
const currentAttributes = new Map()
let str = '' let str = ''
/** /**
* @type {any} * @type {any}
@ -571,12 +695,18 @@ export class YText extends YArray {
function packStr () { function packStr () {
if (str.length > 0) { if (str.length > 0) {
// pack str with attributes to ops // pack str with attributes to ops
/**
* @type {Object<string,any>}
*/
let attributes = {} let attributes = {}
let addAttributes = false let addAttributes = false
for (let [key, value] of currentAttributes) { for (let [key, value] of currentAttributes) {
addAttributes = true addAttributes = true
attributes[key] = value attributes[key] = value
} }
/**
* @type {Object<string,any>}
*/
let op = { insert: str } let op = { insert: str }
if (addAttributes) { if (addAttributes) {
op.attributes = attributes op.attributes = attributes
@ -632,11 +762,14 @@ export class YText extends YArray {
if (text.length <= 0) { if (text.length <= 0) {
return return
} }
this._transact(y => { const y = this._y
let [left, right, currentAttributes] = findPosition(this, index) if (y !== null) {
insertText(y, text, this, left, right, currentAttributes, attributes) y.transact(transaction => {
const {left, right, currentAttributes} = findPosition(transaction, y.store, this, index)
insertText(transaction, this, left, right, currentAttributes, text, attributes)
}) })
} }
}
/** /**
* Inserts an embed at a index. * Inserts an embed at a index.
@ -652,11 +785,14 @@ export class YText extends YArray {
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 => { const y = this._y
let [left, right, currentAttributes] = findPosition(this, index) if (y !== null) {
insertText(y, embed, this, left, right, currentAttributes, attributes) y.transact(transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
insertText(transaction, this, left, right, currentAttributes, embed, attributes)
}) })
} }
}
/** /**
* Deletes text starting from an index. * Deletes text starting from an index.
@ -670,11 +806,14 @@ export class YText extends YArray {
if (length === 0) { if (length === 0) {
return return
} }
this._transact(y => { const y = this._y
let [left, right, currentAttributes] = findPosition(this, index) if (y !== null) {
deleteText(y, length, this, left, right, currentAttributes) y.transact(transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
deleteText(transaction, this, left, right, currentAttributes, length)
}) })
} }
}
/** /**
* Assigns properties to a range of text. * Assigns properties to a range of text.
@ -687,24 +826,21 @@ export class YText extends YArray {
* @public * @public
*/ */
format (index, length, attributes) { format (index, length, attributes) {
this._transact(y => { const y = this._y
let [left, right, currentAttributes] = findPosition(this, index) if (y !== null) {
y.transact(transaction => {
let { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
if (right === null) { if (right === null) {
return return
} }
formatText(y, length, this, left, right, currentAttributes, attributes) formatText(transaction, this, left, right, currentAttributes, length, attributes)
}) })
} }
// TODO: De-duplicate code. The following code is in every type.
/**
* Transform this YText to a readable format.
* Useful for logging as all Items implement this method.
*
* @private
*/
_logString () {
return logItemHelper('YText', this)
} }
} }
/**
* @param {decoding.Decoder} decoder
* @return {YText}
*/
export const readYText = decoder => new YText() export const readYText = decoder => new YText()

View File

@ -2,7 +2,6 @@
import { runTests } from 'lib0/testing.js' import { runTests } from 'lib0/testing.js'
import { isBrowser } from 'lib0/environment.js' import { isBrowser } from 'lib0/environment.js'
import * as log from 'lib0/logging.js' import * as log from 'lib0/logging.js'
import * as deleteStore from './DeleteStore.tests.js'
import * as array from './y-array.tests.js' import * as array from './y-array.tests.js'
import * as map from './y-map.tests.js' import * as map from './y-map.tests.js'
import * as text from './y-text.tests.js' import * as text from './y-text.tests.js'
@ -12,4 +11,4 @@ import * as perf from './perf.js'
if (isBrowser) { if (isBrowser) {
log.createVConsole(document.body) log.createVConsole(document.body)
} }
runTests({ deleteStore, map, array, text, xml, perf }) runTests({ map, array, text, xml, perf })