events can be computed with attributions

This commit is contained in:
Kevin Jahns
2025-05-08 15:18:18 +02:00
parent fc620617df
commit 62422544bc
4 changed files with 91 additions and 57 deletions

View File

@@ -514,7 +514,7 @@ export const typeListGetContent = (type, am) => {
for (let item = type._start; item !== null; cs.length = 0) { for (let item = type._start; item !== null; cs.length = 0) {
// populate cs // populate cs
for (; item !== null && cs.length < 50; item = item.right) { 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++) { for (let i = 0; i < cs.length; i++) {
const { content, deleted, attrs } = cs[i] const { content, deleted, attrs } = cs[i]
@@ -1005,7 +1005,7 @@ export const typeMapGetContent = (parent, am) => {
* @type {Array<import('../internals.js').AttributedContent<any>>} * @type {Array<import('../internals.js').AttributedContent<any>>}
*/ */
const cs = [] const cs = []
am.readContent(cs, item) am.readContent(cs, item, false)
const { deleted, attrs, content } = cs[cs.length - 1] const { deleted, attrs, content } = cs[cs.length - 1]
const c = array.last(content.getContent()) const c = array.last(content.getContent())
const attribution = createAttributionFromAttributionItems(attrs, deleted) const attribution = createAttributionFromAttributionItems(attrs, deleted)
@@ -1021,7 +1021,7 @@ export const typeMapGetContent = (parent, am) => {
* @type {Array<import('../internals.js').AttributedContent<any>>} * @type {Array<import('../internals.js').AttributedContent<any>>}
*/ */
const tmpcs = [] const tmpcs = []
am.readContent(tmpcs, prevItem) am.readContent(tmpcs, prevItem, false)
cs = tmpcs.concat(cs) cs = tmpcs.concat(cs)
if (cs.length === 0 || cs[0].attrs == null) { if (cs.length === 0 || cs[0].attrs == null) {
cs.splice(0, cs.findIndex(c => c.attrs != null)) cs.splice(0, cs.findIndex(c => c.attrs != null))

View File

@@ -640,47 +640,55 @@ export class YTextEvent extends YEvent {
} }
/** /**
* Compute the changes in the delta format. * @param {AbstractAttributionManager} am
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * @return {import('../utils/Delta.js').TextDelta<TextEmbeds>} The Delta representation of this type.
*
* @type {delta.TextDelta<TextEmbeds>}
* *
* @public * @public
*/ */
get delta () { getDelta (am = noAttributionsManager) {
if (this._delta === null) { const ydoc = /** @type {Doc} */ (this.target.doc)
const ydoc = /** @type {Doc} */ (this.target.doc) /**
const d = this._delta = delta.createTextDelta() * @type {import('../utils/Delta.js').TextDelta<TextEmbeds>}
transact(ydoc, transaction => { */
/** const d = delta.createTextDelta()
* @type {import('../utils/Delta.js').FormattingAttributes} transact(ydoc, transaction => {
*/ /**
let currentAttributes = {} // saves all current attributes for insert * @type {import('../utils/Delta.js').FormattingAttributes}
let usingCurrentAttributes = false */
/** let currentAttributes = {} // saves all current attributes for insert
* @type {import('../utils/Delta.js').FormattingAttributes} let usingCurrentAttributes = false
*/ /**
let changedAttributes = {} // saves changed attributes for retain * @type {import('../utils/Delta.js').FormattingAttributes}
let usingChangedAttributes = false */
/** let changedAttributes = {} // saves changed attributes for retain
* @type {import('../utils/Delta.js').FormattingAttributes} let usingChangedAttributes = false
*/ /**
const previousAttributes = {} // The value before changes * @type {import('../utils/Delta.js').FormattingAttributes}
const tr = this.transaction */
let item = this.target._start const previousAttributes = {} // The value before changes
while (item !== null) { const tr = this.transaction
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) { * @type {Array<import('../internals.js').AttributedContent<any>>}
*/
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 ContentType:
case ContentEmbed: case ContentEmbed:
if (freshInsert) { if (freshInsert) {
d.usedAttributes = currentAttributes d.usedAttributes = currentAttributes
usingCurrentAttributes = true usingCurrentAttributes = true
d.insert(item.content.getContent()[0]) d.insert(c.content.getContent()[0], null, attribution)
} else if (freshDelete) { } else if (freshDelete) {
d.delete(1) d.delete(1)
} else if (!item.deleted) { } else if (!c.deleted) {
d.usedAttributes = changedAttributes d.usedAttributes = changedAttributes
usingChangedAttributes = true usingChangedAttributes = true
d.retain(1) d.retain(1)
@@ -690,17 +698,17 @@ export class YTextEvent extends YEvent {
if (freshInsert) { if (freshInsert) {
d.usedAttributes = currentAttributes d.usedAttributes = currentAttributes
usingCurrentAttributes = true usingCurrentAttributes = true
d.insert(/** @type {ContentString} */ (item.content).str) d.insert(/** @type {ContentString} */ (c.content).str, null, attribution)
} else if (freshDelete) { } else if (freshDelete) {
d.delete(item.length) d.delete(c.content.getLength())
} else if (!item.deleted) { } else if (!c.deleted) {
d.usedAttributes = changedAttributes d.usedAttributes = changedAttributes
usingChangedAttributes = true usingChangedAttributes = true
d.retain(item.length) d.retain(c.content.getLength())
} }
break break
case ContentFormat: { case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (item.content) const { key, value } = /** @type {ContentFormat} */ (c.content)
const currAttrVal = currentAttributes[key] ?? null const currAttrVal = currentAttributes[key] ?? null
if (freshDelete || freshInsert) { if (freshDelete || freshInsert) {
// create fresh references // create fresh references
@@ -727,7 +735,7 @@ export class YTextEvent extends YEvent {
changedAttributes[key] = currAttrVal changedAttributes[key] = currAttrVal
currentAttributes[key] = currAttrVal currentAttributes[key] = currAttrVal
previousAttributes[key] = value previousAttributes[key] = value
} else if (!item.deleted) { } else if (!c.deleted) {
// fresh reference to currentAttributes only // fresh reference to currentAttributes only
if (usingCurrentAttributes) { if (usingCurrentAttributes) {
currentAttributes = object.assign({}, currentAttributes) currentAttributes = object.assign({}, currentAttributes)
@@ -739,12 +747,22 @@ export class YTextEvent extends YEvent {
break break
} }
} }
item = item.right
} }
}) }
d.done() })
} return d.done()
return /** @type {any} */ (this._delta) }
/**
* 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<TextEmbeds>}
*
* @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) { for (let item = this._start; item !== null; cs.length = 0) {
// populate cs // populate cs
for (; item !== null && cs.length < 50; item = item.right) { 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++) { for (let i = 0; i < cs.length; i++) {
const { content, deleted, attrs } = cs[i] const { content, deleted, attrs } = cs[i]

View File

@@ -89,8 +89,9 @@ export class AbstractAttributionManager {
/** /**
* @param {Array<AttributedContent<any>>} _contents * @param {Array<AttributedContent<any>>} _contents
* @param {Item} _item * @param {Item} _item
* @param {boolean} _forceRead read content even if it is unattributed and deleted
*/ */
readContent (_contents, _item) { readContent (_contents, _item, _forceRead) {
error.methodUnimplemented() error.methodUnimplemented()
} }
@@ -115,8 +116,9 @@ export class TwosetAttributionManager {
/** /**
* @param {Array<AttributedContent<any>>} contents * @param {Array<AttributedContent<any>>} contents
* @param {Item} item * @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 deleted = item.deleted
const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length) const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length)
let content = slice.length === 1 ? item.content : item.content.copy() let content = slice.length === 1 ? item.content : item.content.copy()
@@ -125,7 +127,7 @@ export class TwosetAttributionManager {
if (s.len < c.getLength()) { if (s.len < c.getLength()) {
content = c.splice(s.len) content = c.splice(s.len)
} }
if (!deleted || s.attrs != null) { if (!deleted || s.attrs != null || forceRead) {
contents.push(new AttributedContent(c, deleted, s.attrs)) contents.push(new AttributedContent(c, deleted, s.attrs))
} }
}) })
@@ -143,9 +145,10 @@ export class NoAttributionsManager {
/** /**
* @param {Array<AttributedContent<any>>} contents * @param {Array<AttributedContent<any>>} contents
* @param {Item} item * @param {Item} item
* @param {boolean} forceRead read content even if it is unattributed and deleted
*/ */
readContent (contents, item) { readContent (contents, item, forceRead) {
if (!item.deleted) { if (!item.deleted || forceRead) {
contents.push(new AttributedContent(item.content, false, null)) contents.push(new AttributedContent(item.content, false, null))
} }
} }
@@ -214,8 +217,9 @@ export class DiffAttributionManager {
/** /**
* @param {Array<AttributedContent<any>>} contents * @param {Array<AttributedContent<any>>} contents
* @param {Item} item * @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 deleted = item.deleted || /** @type {any} */ (item.parent).doc !== this._nextDoc
const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length) const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length)
let content = slice.length === 1 ? item.content : item.content.copy() let content = slice.length === 1 ? item.content : item.content.copy()
@@ -238,7 +242,7 @@ export class DiffAttributionManager {
if (s.len < c.getLength()) { if (s.len < c.getLength()) {
content = c.splice(s.len) content = c.splice(s.len)
} }
if (!deleted || s.attrs != null) { if (!deleted || s.attrs != null || forceRead) {
contents.push(new AttributedContent(c, deleted, s.attrs)) contents.push(new AttributedContent(c, deleted, s.attrs))
} }
}) })
@@ -282,8 +286,9 @@ export class SnapshotAttributionManager {
/** /**
* @param {Array<AttributedContent<any>>} contents * @param {Array<AttributedContent<any>>} contents
* @param {Item} item * @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 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) const slice = this.attrs.slice(item.id, item.length)
let content = slice.length === 1 ? item.content : item.content.copy() let content = slice.length === 1 ? item.content : item.content.copy()
@@ -295,7 +300,7 @@ export class SnapshotAttributionManager {
content = c.splice(s.len) content = c.splice(s.len)
} }
if (nonExistend) return 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 let attrsWithoutChange = s.attrs?.filter(attr => attr.name !== 'change') ?? null
if (s.attrs?.length === 0) { if (s.attrs?.length === 0) {
attrsWithoutChange = null attrsWithoutChange = null

View File

@@ -410,7 +410,7 @@ export class DeltaBuilder extends AbstractDelta {
} }
/** /**
* @return {AbstractDelta<Type,TDeltaOp>} * @return {this}
*/ */
done () { done () {
while (this.lastOp != null && this.lastOp instanceof RetainOp && this.lastOp.attributes === null) { 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<Embeds>> * @extends DeltaBuilder<'text',TextDeltaOp<Embeds>>
*/ */
export class TextDelta extends DeltaBuilder { export class TextDelta extends DeltaBuilder {
@@ -441,6 +441,17 @@ export class TextDelta extends DeltaBuilder {
} }
} }
/**
* @template {'text'|'array'|'custom'} Type
* @template {DeltaOp<any,any>} DeltaOps
* @typedef {AbstractDelta<Type, DeltaOps>} DeltaReadonly
*/
/**
* @template {object} Embeds
* @typedef {DeltaReadonly<'text',TextDeltaOp<Embeds>>} TextDeltaReadonly
*/
/** /**
* @template {object} Embeds * @template {object} Embeds
* @return {TextDelta<Embeds>} * @return {TextDelta<Embeds>}