From 1ce8154d648ec74e0e93a7028db8cd7927f29516 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Wed, 22 Oct 2025 23:25:30 +0200 Subject: [PATCH] fixed several v2 issues --- src/types/AbstractType.js | 115 ++++++++++++++++++++++++++++---------- src/types/YText.js | 35 +----------- src/types/YXmlFragment.js | 1 + src/utils/YEvent.js | 86 ++++++++-------------------- 4 files changed, 112 insertions(+), 125 deletions(-) diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index 36c9b4a0..428055df 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -16,7 +16,12 @@ import { ContentEmbed, getItemCleanStart, noAttributionsManager, - ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, createAttributionFromAttributionItems, AbstractAttributionManager, // eslint-disable-line + transact, + ItemTextListPosition, + insertText, + deleteText, + ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, createAttributionFromAttributionItems, AbstractAttributionManager, + YXmlElement, // eslint-disable-line } from '../internals.js' import * as delta from 'lib0/delta' @@ -178,7 +183,7 @@ export const findMarker = (yarray, index) => { // window.lengths.push(marker.index - pindex) // console.log('distance', marker.index - pindex, 'len', p && p.parent.length) // } - if (marker !== null && math.abs(marker.index - pindex) < /** @type {YText|YArray} */ (p.parent).length / maxSearchMarker) { + if (marker !== null && math.abs(marker.index - pindex) < /** @type {any} */ (p.parent).length / maxSearchMarker) { // adjust existing marker overwriteMarker(marker, p, pindex) return marker @@ -307,6 +312,10 @@ export class AbstractType { * @type {null | Array} */ this._searchMarker = null + /** + * @type {EventDelta?} + */ + this._prelim = null } /** @@ -337,6 +346,10 @@ export class AbstractType { _integrate (y, item) { this.doc = y this._item = item + if (this._prelim) { + this.applyDelta(this._prelim) + this._prelim = null + } } /** @@ -437,6 +450,8 @@ export class AbstractType { * Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the * attribution `{ isDeleted: true, .. }`. * + * @template {boolean} [Deep=false] + * * @param {AbstractAttributionManager} am * @param {Object} [opts] * @param {import('../utils/IdSet.js').IdSet?} [opts.itemsToRender] @@ -445,16 +460,19 @@ export class AbstractType { * @param {Set?} [opts.renderAttrs] - set of attrs to render. if null, render all attributes * @param {boolean} [opts.renderChildren] - if true, retain rendered+attributed deletes only * @param {import('../utils/IdSet.js').IdSet?} [opts.deletedItems] - used for computing prevItem in attributes - * @return {EventDelta} The Delta representation of this type. + * @param {Set|Map|null} [opts.modified] - set of types that should be rendered as modified children + * @param {Deep} [opts.deep] - render child types as delta + * @return {Deep extends true ? ToDeepEventDelta : EventDelta} The Delta representation of this type. * * @public */ - getContent (am = noAttributionsManager, { itemsToRender = null, retainInserts = false, retainDeletes = false, renderAttrs = null, renderChildren = true, deletedItems = null } = {}) { + getContent (am = noAttributionsManager, opts = {}) { + const { itemsToRender = null, retainInserts = false, retainDeletes = false, renderAttrs = null, renderChildren = true, deletedItems = null, modified = null, deep = false } = opts /** * @type {EventDelta} */ - const d = /** @type {any} */ (delta.create()) - typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am, deletedItems, itemsToRender) + const d = /** @type {any} */ (delta.create(this.nodeName || null)) + typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am, deep, modified, deletedItems, itemsToRender) if (renderChildren) { /** * @type {delta.FormattingAttributes} @@ -545,15 +563,21 @@ export class AbstractType { usingCurrentAttributes = true if (c.deleted ? retainDeletes : retainInserts) { d.retain(c.content.getLength(), null, attribution ?? {}) + } else if (deep && c.content.constructor === ContentType) { + d.insert([/** @type {any} */ (c.content).type.getContent(am, opts)], null, attribution) } else { d.insert(c.content.getContent(), null, attribution) } } else if (renderDelete) { d.delete(1) } else if (retainContent) { - d.usedAttributes = changedAttributes - usingChangedAttributes = true - d.retain(1) + if (c.content.constructor === ContentType && modified?.has(/** @type {ContentType} */ (c.content).type)) { + d.modify(/** @type {any} */ (c.content).type.getContent(am, opts)) + } else { + d.usedAttributes = changedAttributes + usingChangedAttributes = true + d.retain(1) + } } break case ContentFormat: { @@ -663,23 +687,52 @@ export class AbstractType { * @return {ToDeepEventDelta} */ getContentDeep (am = noAttributionsManager) { - const d = this.getContent(am) - d.children.forEach(op => { - if (op instanceof delta.InsertOp) { - op.insert = /** @type {any} */ (op.insert.map(ins => - ins instanceof AbstractType - // @ts-ignore - ? ins.getContentDeep(am) - : ins) - ) - } - }) - d.attrs.forEach((op) => { - if (delta.$insertOp.check(op) && op.value instanceof AbstractType) { - op.value = op.value.getContentDeep(am) - } - }) - return /** @type {any} */ (d.done()) + return /** @type {any} */ (this.getContent(am, { deep: true })) + } + + /** + * Apply a {@link Delta} on this shared type. + * + * @param {delta.Delta} d The changes to apply on this element. + * @param {AbstractAttributionManager} am + * + * @public + */ + applyDelta (d, am = noAttributionsManager) { + if (this.doc == null) + (this._prelim || (this._prelim = /** @type {any} */ (delta.create()))).apply(d) + else { + // @todo this was moved here from ytext. Make this more generic + transact(this.doc, transaction => { + const currPos = new ItemTextListPosition(null, this._start, 0, new Map(), am) + for (const op of d.children) { + if (delta.$textOp.check(op)) { + insertText(transaction, this, currPos, op.insert, op.format || {}) + } else if (delta.$insertOp.check(op)) { + for (let i = 0; i < op.insert.length; i++) { + let ins = op.insert[i] + if (delta.$deltaAny.check(ins)) { + if (ins.name != null) { + const t = new YXmlElement(ins.name) + t.applyDelta(ins) + ins = t + } else { + error.unexpectedCase() + } + } + insertText(transaction, this, currPos, ins, op.format || {}) + } + } else if (delta.$retainOp.check(op)) { + currPos.formatText(transaction, this, op.retain, op.format || {}) + } else if (delta.$deleteOp.check(op)) { + deleteText(transaction, currPos, op.delete) + } else if (delta.$modifyOp.check(op)) { + /** @type {ContentType} */ (currPos.right?.content).type.applyDelta(op.modify) + currPos.formatText(transaction, this, 1, op.format || {}) + } + } + }) + } } } @@ -1230,13 +1283,16 @@ export const typeMapGetAll = (parent) => { * @param {YType_} parent * @param {Set?} attrsToRender * @param {import('../internals.js').AbstractAttributionManager} am + * @param {boolean} deep + * @param {Set|Map|null} [modified] - set of types that should be rendered as modified children * @param {import('../utils/IdSet.js').IdSet?} [deletedItems] * @param {import('../utils/IdSet.js').IdSet?} [itemsToRender] * * @private * @function */ -export const typeMapGetDelta = (d, parent, attrsToRender, am, deletedItems, itemsToRender) => { +export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, deletedItems, itemsToRender) => { + // @todo support modified ops! /** * @param {Item} item * @param {string} key @@ -1248,8 +1304,8 @@ export const typeMapGetDelta = (d, parent, attrsToRender, am, deletedItems, item const cs = [] am.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1) const { deleted, attrs, content } = cs[cs.length - 1] - const c = array.last(content.getContent()) const attribution = createAttributionFromAttributionItems(attrs, deleted) + let c = array.last(content.getContent()) if (deleted) { if (itemsToRender == null || itemsToRender.hasId(item.lastId)) { d.unset(key, attribution, c) @@ -1262,6 +1318,9 @@ export const typeMapGetDelta = (d, parent, attrsToRender, am, deletedItems, item // nop } const prevValue = (prevContentItem !== item && itemsToRender?.hasId(prevContentItem.lastId)) ? array.last(prevContentItem.content.getContent()) : undefined + if (deep && c instanceof AbstractType) { + c = c.getContent(am) + } d.set(key, c, attribution, prevValue) } } diff --git a/src/types/YText.js b/src/types/YText.js index bb7fcbf3..8c035e60 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -337,7 +337,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => { * @private * @function **/ -const insertText = (transaction, parent, currPos, text, attributes) => { +export const insertText = (transaction, parent, currPos, text, attributes) => { currPos.currentAttributes.forEach((_val, key) => { if (attributes[key] === undefined) { attributes[key] = null @@ -546,7 +546,7 @@ export const cleanupYTextAfterTransaction = transaction => { * @private * @function */ -const deleteText = (transaction, currPos, length) => { +export const deleteText = (transaction, currPos, length) => { const startLength = length const startAttrs = map.copy(currPos.currentAttributes) const start = currPos.right @@ -745,37 +745,6 @@ export class YText extends AbstractType { return this.toString() } - /** - * Apply a {@link Delta} on this shared YText type. - * - * @param {delta.TextDelta} d The changes to apply on this element. - * @param {AbstractAttributionManager} am - * - * @public - */ - applyDelta (d, am = noAttributionsManager) { - if (this.doc !== null) { - transact(this.doc, transaction => { - const currPos = new ItemTextListPosition(null, this._start, 0, new Map(), am) - for (const op of d.children) { - if (delta.$textOp.check(op)) { - insertText(transaction, this, currPos, op.insert, op.format || {}) - } else if (delta.$insertOp.check(op)) { - for (let i = 0; i < op.insert.length; i++) { - insertText(transaction, this, currPos, op.insert[i], op.format || {}) - } - } else if (delta.$retainOp.check(op)) { - currPos.formatText(transaction, this, op.retain, op.format || {}) - } else if (delta.$deleteOp.check(op)) { - deleteText(transaction, currPos, op.delete) - } - } - }) - } else { - /** @type {Array} */ (this._pending).push(() => this.applyDelta(d)) - } - } - /** * Insert text at a given index. * diff --git a/src/types/YXmlFragment.js b/src/types/YXmlFragment.js index 50677afb..14d5586b 100644 --- a/src/types/YXmlFragment.js +++ b/src/types/YXmlFragment.js @@ -59,6 +59,7 @@ export class YXmlFragment extends AbstractType { constructor () { super() /** + * @todo remove _prelimContent * @type {Array|null} */ this._prelimContent = [] diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index 181d4cfd..ca25faba 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -41,14 +41,14 @@ export class YEvent { * @type {Transaction} */ this.transaction = transaction - /** - * @type {null | Map} - */ - this._keys = null /** * @type {(Target extends AbstractType ? D : delta.Delta)|null} */ this._delta = null + /** + * @type {(Target extends AbstractType ? import('../internals.js').ToDeepEventDelta : delta.Delta)|null} + */ + this._deltaDeep = null /** * @type {Array|null} */ @@ -102,63 +102,6 @@ export class YEvent { return this.transaction.deleteSet.hasId(struct.id) } - /** - * @type {Map} - */ - get keys () { - if (this._keys === null) { - if (this.transaction.doc._transactionCleanups.length === 0) { - throw error.create(errorComputeChanges) - } - const keys = new Map() - const target = this.target - // @ts-ignore - const changed = /** @type Set */ (this.transaction.changed.get(target)) - changed.forEach(key => { - if (key !== null) { - const item = /** @type {Item} */ (target._map.get(key)) - /** - * @type {'delete' | 'add' | 'update'} - */ - let action - let oldValue - if (this.adds(item)) { - let prev = item.left - while (prev !== null && this.adds(prev)) { - prev = prev.left - } - if (this.deletes(item)) { - if (prev !== null && this.deletes(prev)) { - action = 'delete' - oldValue = array.last(prev.content.getContent()) - } else { - return - } - } else { - if (prev !== null && this.deletes(prev)) { - action = 'update' - oldValue = array.last(prev.content.getContent()) - } else { - action = 'add' - oldValue = undefined - } - } - } else { - if (this.deletes(item)) { - action = 'delete' - oldValue = array.last(/** @type {Item} */ item.content.getContent()) - } else { - return // nop - } - } - keys.set(key, { action, oldValue }) - } - }) - this._keys = keys - } - return this._keys - } - /** * Check if a struct is added by this event. * @@ -172,14 +115,18 @@ export class YEvent { } /** + * @template {boolean} [Deep=false] * @param {AbstractAttributionManager} am - * @return {Target extends AbstractType ? D : delta.Delta} The Delta representation of this type. + * @param {object} [opts] + * @param {Deep} [opts.deep] + * @return {Target extends AbstractType ? (Deep extends true ? import('../internals.js').ToDeepEventDelta : D) : delta.Delta} The Delta representation of this type. * * @public */ - getDelta (am = noAttributionsManager) { + getDelta (am = noAttributionsManager, { deep } = {}) { const itemsToRender = mergeIdSets([diffIdSet(this.transaction.insertSet, this.transaction.deleteSet), diffIdSet(this.transaction.deleteSet, this.transaction.insertSet)]) - return /** @type {any} */ (this.target.getContent(am, { itemsToRender, retainDeletes: true, renderAttrs: this.keysChanged, renderChildren: this.childListChanged, deletedItems: this.transaction.deleteSet })) + const modified = deep ? this.transaction.changedParentTypes : null + return /** @type {any} */ (this.target.getContent(am, { itemsToRender, retainDeletes: true, renderAttrs: this.keysChanged, renderChildren: deep || this.childListChanged, deletedItems: this.transaction.deleteSet, deep: !!deep, modified })) } /** @@ -192,6 +139,17 @@ export class YEvent { get delta () { return /** @type {any} */ (this._delta ?? (this._delta = this.getDelta())) } + + /** + * Compute the changes in the delta format. + * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. + * + * @type {Target extends AbstractType ? D : delta.Delta} The Delta representation of this type. + * @public + */ + get deltaDeep () { + return /** @type {any} */ (this._deltaDeep ?? (this._deltaDeep = this.getDelta(noAttributionsManager, { deep: true }))) + } } /**