diff --git a/src/types/YText.js b/src/types/YText.js index b42a766c..74554cea 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -1011,18 +1011,22 @@ export class YText extends AbstractType { for (let i = 0; i < cs.length; i++) { const { content, deleted, attrs } = cs[i] /** - * @type {{ [key: string]: any }?} + * @type {import('../utils/Delta.js').Attribution?} */ let attributions = null if (attrs != null) { attributions = {} - attributions.changeType = deleted ? 'delete' : 'insert' + if (deleted) { + attributions.delete = [] + } else { + attributions.insert = [] + } attrs.forEach(attr => { switch (attr.name) { - case 'insertedBy': - case 'deletedBy': - case 'suggestedBy': { - const as = /** @type {any} */ (attributions) + case 'insert': + case 'delete': + case 'suggest': { + const as = /** @type {import('../utils/Delta.js').Attribution} */ (attributions) const ls = as[attr.name] = as[attr.name] ?? [] ls.push(attr.val) break @@ -1046,15 +1050,37 @@ export class YText extends AbstractType { break } case ContentFormat: + const contentFormat = /** @type {ContentFormat} */ (content) if (attributions != null) { - if (deleted) { + /** + * @type {import('../utils/Delta.js').Attribution} + */ + const formattingAttributions = object.assign({}, d.usedAttribution) + const attributesChanged = /** @type {{ [key: string]: Array }} */ (formattingAttributions.attributes = object.assign({}, formattingAttributions.attributes ?? {})) + if (contentFormat.value === null) { + delete attributesChanged[contentFormat.key] + } else { + const by = attributesChanged[contentFormat.key] = attributesChanged[contentFormat.key]?.slice() ?? [] + by.push(...((deleted ? attributions.delete : attributions.insert) ?? [])) + const attributedAt = (deleted ? attributions.deletedAt : attributions.insertedAt) + if (attributedAt) formattingAttributions.attributedAt = attributedAt + } + if (object.isEmpty(attributesChanged)) { d.useAttribution(null) } else { - attributions.formattedBy = (deleted ? attributions.deletedBy : attributions.insertedBy) ?? [] - attributions.changeType = 'format' - delete attributions.deletedBy - delete attributions.insertedBy - d.useAttribution(attributions) + const attributedAt = (deleted ? attributions.deletedAt : attributions.insertedAt) + if (attributedAt != null) formattingAttributions.attributedAt = attributedAt + d.useAttribution(formattingAttributions) + } + } + if (!deleted) { + const currAttrs = d.usedAttributes + if (contentFormat.value == null) { + const nextAttrs = object.assign({}, currAttrs) + delete nextAttrs[contentFormat.key] + d.useAttributes(nextAttrs) + } else { + d.useAttributes(object.assign({}, currAttrs, { [contentFormat.key]: contentFormat.value })) } } break diff --git a/src/utils/Delta.js b/src/utils/Delta.js index 9f31fd0a..54712c19 100644 --- a/src/utils/Delta.js +++ b/src/utils/Delta.js @@ -10,13 +10,15 @@ import * as fun from 'lib0/function' */ /** - * @todo specify this better - * * @typedef {Object} Attribution - * @property {boolean} [Attribution.isDeleted] - * @property {boolean} [Attribution.isAdded] - * @property {string} [Attribution.creator] - * @property {number} [Attribution.timestamp] + * @property {Array} [Attribution.insert] + * @property {number} [Attribution.insertedAt] + * @property {Array} [Attribution.suggest] + * @property {number} [Attribution.suggestedAt] + * @property {Array} [Attribution.delete] + * @property {number} [Attribution.deletedAt] + * @property {{ [key: string]: Array }} [Attribution.attributes] + * @property {number} [Attribution.attributedAt] */ export class InsertOp { @@ -136,15 +138,13 @@ export class DeltaBuilder extends Delta { constructor () { super() /** - * @private * @type {FormattingAttributes?} */ - this._useAttributes = null + this.usedAttributes = null /** - * @private * @type {Attribution?} */ - this._useAttribution = null + this.usedAttribution = null /** * @private * @type {DeltaOp?} @@ -157,8 +157,8 @@ export class DeltaBuilder extends Delta { * @return {this} */ useAttributes (attributes) { - if (this._useAttributes === attributes) return this - this._useAttributes = attributes && object.assign({}, attributes) + if (this.usedAttributes === attributes) return this + this.usedAttributes = attributes && object.assign({}, attributes) return this } @@ -166,8 +166,8 @@ export class DeltaBuilder extends Delta { * @param {Attribution?} attribution */ useAttribution (attribution) { - if (this._useAttribution === attribution) return this - this._useAttribution = attribution && object.assign({}, attribution) + if (this.usedAttribution === attribution) return this + this.usedAttribution = attribution && object.assign({}, attribution) return this } @@ -178,8 +178,8 @@ export class DeltaBuilder extends Delta { * @return {this} */ insert (insert, attributes = null, attribution = null) { - const mergedAttributes = mergeAttrs(this._useAttributes, attributes) - const mergedAttribution = mergeAttrs(this._useAttribution, attribution) + const mergedAttributes = mergeAttrs(this.usedAttributes, attributes) + const mergedAttribution = mergeAttrs(this.usedAttribution, attribution) if (this._lastOp instanceof InsertOp && fun.equalityDeep(mergedAttributes, this._lastOp.attributes) && fun.equalityDeep(mergedAttribution, this._lastOp.attribution)) { this._lastOp.insert += insert } else { @@ -195,8 +195,8 @@ export class DeltaBuilder extends Delta { * @return {this} */ retain (retain, attributes = null, attribution = null) { - const mergedAttributes = mergeAttrs(this._useAttributes, attributes) - const mergedAttribution = mergeAttrs(this._useAttribution, attribution) + const mergedAttributes = mergeAttrs(this.usedAttributes, attributes) + const mergedAttribution = mergeAttrs(this.usedAttribution, attribution) if (this._lastOp instanceof RetainOp && fun.equalityDeep(mergedAttributes, this._lastOp.attributes) && fun.equalityDeep(mergedAttribution, this._lastOp.attribution)) { this._lastOp.retain += retain } else { diff --git a/src/utils/IdMap.js b/src/utils/IdMap.js index 820f3fa1..d08e6794 100644 --- a/src/utils/IdMap.js +++ b/src/utils/IdMap.js @@ -121,6 +121,8 @@ export class AttrRange { export const createMaybeAttrRange = (clock, len, attrs) => new AttrRange(clock, len, /** @type {any} */ (attrs)) /** + * Whenever this is instantiated, it must receive a fresh array of ops, not something copied. + * * @template Attrs */ export class AttrRanges { diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index c92c248a..bb2f34c4 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -2302,9 +2302,9 @@ export const testDeleteFormatting = _tc => { } /** - * @param {t.TestCase} tc + * @param {t.TestCase} _tc */ -export const testAttributedContent = tc => { +export const testAttributedContent = _tc => { const ydoc = new Y.Doc({ gc: false }) const ytext = ydoc.getText() ytext.insert(0, 'Hello World!') @@ -2312,11 +2312,20 @@ export const testAttributedContent = tc => { ydoc.on('afterTransaction', tr => { am = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, [])) }) - ytext.applyDelta([{ retain: 6 }, { delete: 5 }, { insert: 'attributions' }]) - const attributedContent = ytext.getContent(am) - const expectedContent = delta.create().insert('Hello ').insert('World', {}, { changeType: 'delete' }).insert('attributions', {}, { changeType: 'insert' }).insert('!') - t.assert(attributedContent.equals(expectedContent)) - debugger + t.group('insert / delete / format', () => { + ytext.applyDelta([{ retain: 4, attributes: { italic: true } }, { retain: 2 }, { delete: 5 }, { insert: 'attributions' }]) + let expectedContent = delta.create().insert('Hell', { italic: true }, { attributes: { italic: [] } }).insert('o ').insert('World', {}, { delete: [] }).insert('attributions', {}, { insert: [] }).insert('!') + let attributedContent = ytext.getContent(am) + console.log(attributedContent.toJSON().ops) + t.assert(attributedContent.equals(expectedContent)) + }) + t.group('unformat', () => { + ytext.applyDelta([{retain: 5, attributes: { italic: null }}]) + let expectedContent = delta.create().insert('Hell', null, { attributes: { italic: [] } }).insert('o attributions!') + let attributedContent = ytext.getContent(am) + console.log(attributedContent.toJSON().ops) + t.assert(attributedContent.equals(expectedContent)) + }) } // RANDOM TESTS