diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index ac81ec7e..31e9acc6 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -514,7 +514,7 @@ export const typeListGetContent = (type, am) => { for (let item = type._start; item !== null; cs.length = 0) { // populate cs for (; item !== null && cs.length < 50; item = item.right) { - am.readContent(cs, item) + am.readContent(cs, item, false) } for (let i = 0; i < cs.length; i++) { const { content, deleted, attrs } = cs[i] @@ -1005,7 +1005,7 @@ export const typeMapGetContent = (parent, am) => { * @type {Array>} */ const cs = [] - am.readContent(cs, item) + am.readContent(cs, item, false) const { deleted, attrs, content } = cs[cs.length - 1] const c = array.last(content.getContent()) const attribution = createAttributionFromAttributionItems(attrs, deleted) @@ -1021,7 +1021,7 @@ export const typeMapGetContent = (parent, am) => { * @type {Array>} */ const tmpcs = [] - am.readContent(tmpcs, prevItem) + am.readContent(tmpcs, prevItem, false) cs = tmpcs.concat(cs) if (cs.length === 0 || cs[0].attrs == null) { cs.splice(0, cs.findIndex(c => c.attrs != null)) diff --git a/src/types/YText.js b/src/types/YText.js index f556bcba..22c73279 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -640,47 +640,55 @@ 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 {delta.TextDelta} + * @param {AbstractAttributionManager} am + * @return {import('../utils/Delta.js').TextDelta} The Delta representation of this type. * * @public */ - get delta () { - if (this._delta === null) { - const ydoc = /** @type {Doc} */ (this.target.doc) - const d = this._delta = delta.createTextDelta() - transact(ydoc, transaction => { - /** - * @type {import('../utils/Delta.js').FormattingAttributes} - */ - let currentAttributes = {} // saves all current attributes for insert - let usingCurrentAttributes = false - /** - * @type {import('../utils/Delta.js').FormattingAttributes} - */ - let changedAttributes = {} // saves changed attributes for retain - let usingChangedAttributes = false - /** - * @type {import('../utils/Delta.js').FormattingAttributes} - */ - const previousAttributes = {} // The value before changes - const tr = this.transaction - let item = this.target._start - while (item !== null) { - const freshDelete = item.deleted && tr.deleteSet.hasId(item.id) && !tr.insertSet.hasId(item.id) - const freshInsert = !item.deleted && tr.insertSet.hasId(item.id) - switch (item.content.constructor) { + getDelta (am = noAttributionsManager) { + const ydoc = /** @type {Doc} */ (this.target.doc) + /** + * @type {import('../utils/Delta.js').TextDelta} + */ + const d = delta.createTextDelta() + transact(ydoc, transaction => { + /** + * @type {import('../utils/Delta.js').FormattingAttributes} + */ + let currentAttributes = {} // saves all current attributes for insert + let usingCurrentAttributes = false + /** + * @type {import('../utils/Delta.js').FormattingAttributes} + */ + let changedAttributes = {} // saves changed attributes for retain + let usingChangedAttributes = false + /** + * @type {import('../utils/Delta.js').FormattingAttributes} + */ + const previousAttributes = {} // The value before changes + const tr = this.transaction + + /** + * @type {Array>} + */ + const cs = [] + for (let item = this.target._start; item !== null; cs.length = 0, item = item.right) { + const freshDelete = item.deleted && tr.deleteSet.hasId(item.id) && !tr.insertSet.hasId(item.id) + const freshInsert = !item.deleted && tr.insertSet.hasId(item.id) + am.readContent(cs, item, freshDelete) // do item.right after calling this + for (let i = 0; i < cs.length; i++) { + const c = cs[i] + const attribution = createAttributionFromAttributionItems(c.attrs, c.deleted) + switch (c.content.constructor) { case ContentType: case ContentEmbed: if (freshInsert) { d.usedAttributes = currentAttributes usingCurrentAttributes = true - d.insert(item.content.getContent()[0]) + d.insert(c.content.getContent()[0], null, attribution) } else if (freshDelete) { d.delete(1) - } else if (!item.deleted) { + } else if (!c.deleted) { d.usedAttributes = changedAttributes usingChangedAttributes = true d.retain(1) @@ -690,17 +698,17 @@ export class YTextEvent extends YEvent { if (freshInsert) { d.usedAttributes = currentAttributes usingCurrentAttributes = true - d.insert(/** @type {ContentString} */ (item.content).str) + d.insert(/** @type {ContentString} */ (c.content).str, null, attribution) } else if (freshDelete) { - d.delete(item.length) - } else if (!item.deleted) { + d.delete(c.content.getLength()) + } else if (!c.deleted) { d.usedAttributes = changedAttributes usingChangedAttributes = true - d.retain(item.length) + d.retain(c.content.getLength()) } break case ContentFormat: { - const { key, value } = /** @type {ContentFormat} */ (item.content) + const { key, value } = /** @type {ContentFormat} */ (c.content) const currAttrVal = currentAttributes[key] ?? null if (freshDelete || freshInsert) { // create fresh references @@ -727,7 +735,7 @@ export class YTextEvent extends YEvent { changedAttributes[key] = currAttrVal currentAttributes[key] = currAttrVal previousAttributes[key] = value - } else if (!item.deleted) { + } else if (!c.deleted) { // fresh reference to currentAttributes only if (usingCurrentAttributes) { currentAttributes = object.assign({}, currentAttributes) @@ -739,12 +747,22 @@ export class YTextEvent extends YEvent { break } } - item = item.right } - }) - d.done() - } - return /** @type {any} */ (this._delta) + } + }) + return d.done() + } + + /** + * Compute the changes in the delta format. + * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. + * + * @type {delta.TextDelta} + * + * @public + */ + get delta () { + return this._delta ?? (this._delta = this.getDelta()) } } @@ -961,7 +979,7 @@ export class YText extends AbstractType { for (let item = this._start; item !== null; cs.length = 0) { // populate cs for (; item !== null && cs.length < 50; item = item.right) { - am.readContent(cs, item) + am.readContent(cs, item, false) } for (let i = 0; i < cs.length; i++) { const { content, deleted, attrs } = cs[i] diff --git a/src/utils/AttributionManager.js b/src/utils/AttributionManager.js index 8f10ece0..cecadb5c 100644 --- a/src/utils/AttributionManager.js +++ b/src/utils/AttributionManager.js @@ -89,8 +89,9 @@ export class AbstractAttributionManager { /** * @param {Array>} _contents * @param {Item} _item + * @param {boolean} _forceRead read content even if it is unattributed and deleted */ - readContent (_contents, _item) { + readContent (_contents, _item, _forceRead) { error.methodUnimplemented() } @@ -115,8 +116,9 @@ export class TwosetAttributionManager { /** * @param {Array>} contents * @param {Item} item + * @param {boolean} forceRead read content even if it is unattributed and deleted */ - readContent (contents, item) { + readContent (contents, item, forceRead) { const deleted = item.deleted const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length) let content = slice.length === 1 ? item.content : item.content.copy() @@ -125,7 +127,7 @@ export class TwosetAttributionManager { if (s.len < c.getLength()) { content = c.splice(s.len) } - if (!deleted || s.attrs != null) { + if (!deleted || s.attrs != null || forceRead) { contents.push(new AttributedContent(c, deleted, s.attrs)) } }) @@ -143,9 +145,10 @@ export class NoAttributionsManager { /** * @param {Array>} contents * @param {Item} item + * @param {boolean} forceRead read content even if it is unattributed and deleted */ - readContent (contents, item) { - if (!item.deleted) { + readContent (contents, item, forceRead) { + if (!item.deleted || forceRead) { contents.push(new AttributedContent(item.content, false, null)) } } @@ -214,8 +217,9 @@ export class DiffAttributionManager { /** * @param {Array>} contents * @param {Item} item + * @param {boolean} forceRead read content even if it is unattributed and deleted */ - readContent (contents, item) { + readContent (contents, item, forceRead) { const deleted = item.deleted || /** @type {any} */ (item.parent).doc !== this._nextDoc const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length) let content = slice.length === 1 ? item.content : item.content.copy() @@ -238,7 +242,7 @@ export class DiffAttributionManager { if (s.len < c.getLength()) { content = c.splice(s.len) } - if (!deleted || s.attrs != null) { + if (!deleted || s.attrs != null || forceRead) { contents.push(new AttributedContent(c, deleted, s.attrs)) } }) @@ -282,8 +286,9 @@ export class SnapshotAttributionManager { /** * @param {Array>} contents * @param {Item} item + * @param {boolean} forceRead read content even if it is unattributed and deleted */ - readContent (contents, item) { + readContent (contents, item, forceRead) { if ((this.nextSnapshot.sv.get(item.id.client) ?? 0) <= item.id.clock) return // future item that should not be displayed const slice = this.attrs.slice(item.id, item.length) let content = slice.length === 1 ? item.content : item.content.copy() @@ -295,7 +300,7 @@ export class SnapshotAttributionManager { content = c.splice(s.len) } if (nonExistend) return - if (!deleted || (s.attrs != null && s.attrs.length > 0)) { + if (!deleted || forceRead || (s.attrs != null && s.attrs.length > 0)) { let attrsWithoutChange = s.attrs?.filter(attr => attr.name !== 'change') ?? null if (s.attrs?.length === 0) { attrsWithoutChange = null diff --git a/src/utils/Delta.js b/src/utils/Delta.js index d7a9b8a5..6f820667 100644 --- a/src/utils/Delta.js +++ b/src/utils/Delta.js @@ -410,7 +410,7 @@ export class DeltaBuilder extends AbstractDelta { } /** - * @return {AbstractDelta} + * @return {this} */ done () { while (this.lastOp != null && this.lastOp instanceof RetainOp && this.lastOp.attributes === null) { @@ -432,7 +432,7 @@ export class ArrayDelta extends DeltaBuilder { } /** - * @template {{ [key:string]: any }} Embeds + * @template {object} Embeds * @extends DeltaBuilder<'text',TextDeltaOp> */ export class TextDelta extends DeltaBuilder { @@ -441,6 +441,17 @@ export class TextDelta extends DeltaBuilder { } } +/** + * @template {'text'|'array'|'custom'} Type + * @template {DeltaOp} DeltaOps + * @typedef {AbstractDelta} DeltaReadonly + */ + +/** + * @template {object} Embeds + * @typedef {DeltaReadonly<'text',TextDeltaOp>} TextDeltaReadonly + */ + /** * @template {object} Embeds * @return {TextDelta}