diff --git a/src/types/YText.js b/src/types/YText.js index 6ded2658..9f1edae6 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -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} parent * @param {ItemTextListPosition} currPos - * @param {string|object} text + * @param {string|object|AbstractType} text * @param {Object} 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,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} + * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string|AbstractType|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}>} + * @type {Array<{insert?:string|object|AbstractType, delete?:number, retain?:number, attributes?: Object}>} * * @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}>} + * @type {Array<{insert?:string|object|AbstractType, delete?:number, retain?:number, attributes?: Object}>} */ 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} */ const op = { - insert: /** @type {ContentEmbed} */ (n.content).embed + insert: n.content.getContent()[0] } if (currentAttributes.size > 0) { const attrs = /** @type {Object} */ ({}) @@ -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} 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 => { diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index 89d89742..3501b587 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -40,7 +40,7 @@ export class YEvent { */ this._keys = null /** - * @type {null | Array<{ insert?: string | Array, retain?: number, delete?: number, attributes?: Object }>} + * @type {null | Array<{ insert?: string | Array | object | AbstractType, retain?: number, delete?: number, attributes?: Object }>} */ this._delta = null } @@ -129,7 +129,7 @@ export class YEvent { } /** - * @type {Array<{insert?: string | Array, retain?: number, delete?: number, attributes?: Object}>} + * @type {Array<{insert?: string | Array | object | AbstractType, retain?: number, delete?: number, attributes?: Object}>} */ get delta () { return this.changes.delta diff --git a/tests/testHelper.js b/tests/testHelper.js index 6cb2ba35..c325ddca 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -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) diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index 8a900da5..e21bfb6d 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -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} */ (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