allow types as Y.Text embeds

This commit is contained in:
Kevin Jahns 2021-09-25 11:51:08 +02:00
parent df9bfbe778
commit 0ec67170d3
4 changed files with 72 additions and 37 deletions

View File

@ -32,6 +32,7 @@ import {
import * as object from 'lib0/object'
import * as map from 'lib0/map'
import * as error from 'lib0/error'
import { ContentType } from 'yjs'
/**
* @param {any} a
@ -62,17 +63,16 @@ export class ItemTextListPosition {
error.unexpectedCase()
}
switch (this.right.content.constructor) {
case ContentEmbed:
case ContentString:
if (!this.right.deleted) {
this.index += this.right.length
}
break
case ContentFormat:
if (!this.right.deleted) {
updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content))
}
break
default:
if (!this.right.deleted) {
this.index += this.right.length
}
break
}
this.left = this.right
this.right = this.right.right
@ -91,8 +91,12 @@ export class ItemTextListPosition {
const findNextPosition = (transaction, pos, count) => {
while (pos.right !== null && count > 0) {
switch (pos.right.content.constructor) {
case ContentEmbed:
case ContentString:
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
default:
if (!pos.right.deleted) {
if (count < pos.right.length) {
// split right
@ -102,11 +106,6 @@ const findNextPosition = (transaction, pos, count) => {
count -= pos.right.length
}
break
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
}
pos.left = pos.right
pos.right = pos.right.right
@ -245,7 +244,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemTextListPosition} currPos
* @param {string|object} text
* @param {string|object|AbstractType<any>} text
* @param {Object<string,any>} attributes
*
* @private
@ -262,7 +261,7 @@ const insertText = (transaction, parent, currPos, text, attributes) => {
minimizeAttributeChanges(currPos, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// insert content
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : new ContentEmbed(text)
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text))
let { left, right, index } = currPos
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
@ -308,8 +307,7 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
}
break
}
case ContentEmbed:
case ContentString:
default:
if (length < currPos.right.length) {
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
}
@ -348,7 +346,7 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
* @function
*/
const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttributes) => {
while (end && end.content.constructor !== ContentString && end.content.constructor !== ContentEmbed) {
while (end && (!end.countable || end.deleted)) {
if (!end.deleted && end.content.constructor === ContentFormat) {
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
}
@ -381,12 +379,12 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
*/
const cleanupContextlessFormattingGap = (transaction, item) => {
// iterate until item.right is null or content
while (item && item.right && (item.right.deleted || (item.right.content.constructor !== ContentString && item.right.content.constructor !== ContentEmbed))) {
while (item && item.right && (item.right.deleted || !item.right.countable)) {
item = item.right
}
const attrs = new Set()
// iterate back until a content item is found
while (item && (item.deleted || (item.content.constructor !== ContentString && item.content.constructor !== ContentEmbed))) {
while (item && (item.deleted || !item.countable)) {
if (!item.deleted && item.content.constructor === ContentFormat) {
const key = /** @type {ContentFormat} */ (item.content).key
if (attrs.has(key)) {
@ -424,8 +422,7 @@ export const cleanupYTextFormatting = type => {
case ContentFormat:
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content))
break
case ContentEmbed:
case ContentString:
default:
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
startAttributes = map.copy(currentAttributes)
start = end
@ -454,6 +451,7 @@ const deleteText = (transaction, currPos, length) => {
while (length > 0 && currPos.right !== null) {
if (currPos.right.deleted === false) {
switch (currPos.right.content.constructor) {
case ContentType:
case ContentEmbed:
case ContentString:
if (length < currPos.right.length) {
@ -540,7 +538,7 @@ export class YTextEvent extends YEvent {
get changes () {
if (this._changes === null) {
/**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string|AbstractType<any>|object, delete?:number, retain?:number}>}}
*/
const changes = {
keys: this.keys,
@ -557,7 +555,7 @@ export class YTextEvent extends YEvent {
* Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
*
* @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>}
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
*
* @public
*/
@ -565,7 +563,7 @@ export class YTextEvent extends YEvent {
if (this._delta === null) {
const y = /** @type {Doc} */ (this.target.doc)
/**
* @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>}
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
*/
const delta = []
transact(y, transaction => {
@ -626,12 +624,13 @@ export class YTextEvent extends YEvent {
}
while (item !== null) {
switch (item.content.constructor) {
case ContentType:
case ContentEmbed:
if (this.adds(item)) {
if (!this.deletes(item)) {
addOp()
action = 'insert'
insert = /** @type {ContentEmbed} */ (item.content).embed
insert = item.content.getContent()[0]
addOp()
}
} else if (this.deletes(item)) {
@ -1008,13 +1007,14 @@ export class YText extends AbstractType {
str += /** @type {ContentString} */ (n.content).str
break
}
case ContentType:
case ContentEmbed: {
packStr()
/**
* @type {Object<string,any>}
*/
const op = {
insert: /** @type {ContentEmbed} */ (n.content).embed
insert: n.content.getContent()[0]
}
if (currentAttributes.size > 0) {
const attrs = /** @type {Object<string,any>} */ ({})
@ -1075,16 +1075,13 @@ export class YText extends AbstractType {
* Inserts an embed at a index.
*
* @param {number} index The index to insert the embed at.
* @param {Object} embed The Object that represents the embed.
* @param {Object | AbstractType<any>} embed The Object that represents the embed.
* @param {TextAttributes} attributes Attribute information to apply on the
* embed
*
* @public
*/
insertEmbed (index, embed, attributes = {}) {
if (embed.constructor !== Object) {
throw new Error('Embed must be an Object')
}
const y = this.doc
if (y !== null) {
transact(y, transaction => {

View File

@ -40,7 +40,7 @@ export class YEvent {
*/
this._keys = null
/**
* @type {null | Array<{ insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
*/
this._delta = null
}
@ -129,7 +129,7 @@ export class YEvent {
}
/**
* @type {Array<{insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
*/
get delta () {
return this.changes.delta

View File

@ -362,7 +362,14 @@ export const compare = users => {
t.compare(userMapValues[i], userMapValues[i + 1])
t.compare(userXmlValues[i], userXmlValues[i + 1])
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
t.compare(userTextValues[i], userTextValues[i + 1])
t.compare(userTextValues[i], userTextValues[i + 1], '', (constructor, a, b) => {
if (a instanceof Y.AbstractType) {
t.compare(a.toJSON(), b.toJSON())
} else if (a !== b) {
t.fail('Deltas dont match')
}
return true
})
t.compare(Y.getStateVector(users[i].store), Y.getStateVector(users[i + 1].store))
compareDS(Y.createDeleteSetFromStructStore(users[i].store), Y.createDeleteSetFromStructStore(users[i + 1].store))
compareStructStores(users[i].store, users[i + 1].store)

View File

@ -151,6 +151,29 @@ export const testGetDeltaWithEmbeds = tc => {
}])
}
/**
* @param {t.TestCase} tc
*/
export const testTypesAsEmbed = tc => {
const { text0, text1, testConnector } = init(tc, { users: 2 })
text0.applyDelta([{
insert: new Y.YMap([['key', 'val']])
}])
t.compare(text0.toDelta()[0].insert.toJSON(), { key: 'val' })
let firedEvent = false
text1.observe(event => {
const d = event.delta
t.assert(d.length === 1)
t.compare(d.map(x => /** @type {Y.AbstractType<any>} */ (x.insert).toJSON()), [{ key: 'val' }])
firedEvent = true
})
testConnector.flushAllMessages()
const delta = text1.toDelta()
t.assert(delta.length === 1)
t.compare(delta[0].insert.toJSON(), { key: 'val' })
t.assert(firedEvent, 'fired the event observer containing a Type-Embed')
}
/**
* @param {t.TestCase} tc
*/
@ -628,7 +651,11 @@ const qChanges = [
(y, gen) => { // insert embed
const ytext = y.getText('text')
const insertPos = prng.int32(gen, 0, ytext.length)
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
if (prng.bool(gen)) {
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
} else {
ytext.insertEmbed(insertPos, new Y.YMap())
}
},
/**
* @param {Y.Doc} y
@ -675,8 +702,12 @@ const qChanges = [
*/
const checkResult = result => {
for (let i = 1; i < result.testObjects.length; i++) {
const p1 = result.users[i].getText('text').toDelta()
const p2 = result.users[i].getText('text').toDelta()
/**
* @param {any} d
*/
const typeToObject = d => d.insert instanceof Y.AbstractType ? d.insert.toJSON() : d
const p1 = result.users[i].getText('text').toDelta().map(typeToObject)
const p2 = result.users[i].getText('text').toDelta().map(typeToObject)
t.compare(p1, p2)
}
// Uncomment this to find formatting-cleanup issues