UNPKG

@y/y

Version:

Shared Editing Library

195 lines (186 loc) • 6.63 kB
import * as map from 'lib0/map' import * as set from 'lib0/set' import { diffIdSet, intersectSets, mergeIdSets } from './ids.js' import { createAbsolutePositionFromRelativePosition, createRelativePosition } from './RelativePosition.js' /** * @template {DeltaConf} DConf * YEvent describes the changes on a YNode. */ export class YEvent { /** * @param {YNode<DConf>} target The changed type. * @param {import('./Transaction.js').Transaction} transaction * @param {Set<any>?} subs The keys that changed */ constructor (target, transaction, subs) { /** * The type on which this event was created on. * @type {import('../ynode.js').YNode<DConf>} */ this.target = target /** * The current target on which the observe callback is called. * @type {YNode<any>} */ this.currentTarget = target /** * The transaction that triggered this event. * @type {import('./Transaction.js').Transaction} */ this.transaction = transaction /** * @type {Delta<import('../ynode.js').DeltaConfDeltaToYNode<DConf>>|null} */ this._delta = null /** * @type {Delta<DConf>|null} */ this._deltaDeep = null /** * Whether the children changed. * @type {Boolean} * @private */ this.childListChanged = false /** * Set of all changed attributes. * @type {Set<string>} */ this.keysChanged = new Set() subs?.forEach((sub) => { if (sub === null) { this.childListChanged = true } else { this.keysChanged.add(sub) } }) } /** * Check if a struct is deleted by this event. * * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. * * @param {AbstractStruct} struct * @return {boolean} */ deletes (struct) { return this.transaction.deleteSet.hasId(struct.id) } /** * Check if a struct is added by this event. * * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. * * @param {AbstractStruct} struct * @return {boolean} */ adds (struct) { return this.transaction.insertSet.hasId(struct.id) } /** * @template {boolean} [Deep=false] * @param {object} [opts] * @param {AbstractRenderer?} [opts.renderer] - renders the content (with attributions); defaults to the target type's active renderer (see {@link YNode#useRenderer}), i.e. `null` (render as-is) unless changed * @param {Deep} [opts.deep] * @return {Deep extends true ? Delta<DConf> : Delta<import('../ynode.js').DeltaConfDeltaToYNode<DConf>>} The Delta representation of this type. * * @public */ getDelta ({ renderer = this.target._renderer, deep } = {}) { const insertSet = this.transaction.insertSet const deleteSet = this.transaction.deleteSet // content that was both inserted AND deleted by this transaction is normally invisible — but a // renderer may still render it (e.g. remote content integrated under a suggestion-deleted // parent is auto-deleted yet rendered with a delete attribution). Render // `(I ∪ D) − ((I ∩ D) − renderer.attributed)`: the symmetric difference plus the attributed // part of the intersection. const both = intersectSets(insertSet, deleteSet) const itemsToRender = mergeIdSets( (both.isEmpty() || renderer === null) ? [diffIdSet(insertSet, deleteSet), diffIdSet(deleteSet, insertSet)] : [diffIdSet(insertSet, deleteSet), diffIdSet(deleteSet, insertSet), intersectSets(both, renderer.attributed)] ) /** * @todo this should be done only one in the transaction step * * @type {Map<YNode,Set<string|null>>|null} */ let modified = this.transaction.changed if (deep) { // need to add deep changes to copy of modified const dchanged = new Map() modified.forEach((attrs, type) => { dchanged.set(type, new Set(attrs)) }) for (let m of modified.keys()) { while (m._item != null) { const item = m._item const ms = map.setIfUndefined(dchanged, item?.parent, set.create) if (item && !ms.has(item.parentSub)) { ms.add(item.parentSub) m = /** @type {any} */ (item.parent) } else { break } } } modified = dchanged } return /** @type {any} */ (this.target.toDelta({ renderer, itemsToRender, retainDeletes: true, insertedItems: insertSet, deep: !!deep, modified })) } /** * 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<import('../ynode.js').DeltaConfDeltaToYNode<DConf>>} The Delta representation of this type. * @public */ get delta () { return /** @type {any} */ (this._delta ?? (this._delta = this.getDelta().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<DConf>} The Delta representation of this type. * @public */ get deltaDeep () { return /** @type {any} */ (this._deltaDeep ?? (this._deltaDeep = /** @type {any} */ (this.getDelta({ deep: true })))) } } /** * Compute the path from this type to the specified target. * * @example * // `child` should be accessible via `type.get(path[0]).get(path[1])..` * const path = type.getPathTo(child) * // assuming `type instanceof YArray` * console.log(path) // might look like => [2, 'key1'] * child === type.get(path[0]).get(path[1]) * * @param {YNode} parent * @param {YNode} child target * @param {AbstractRenderer?} renderer * @return {Array<string|number>} Path to the target * * @private * @function */ export const getPathTo = (parent, child, renderer = null) => { const path = [] const doc = /** @type {Doc} */ (parent.doc) while (child._item !== null && child !== parent) { if (child._item.parentSub !== null) { // parent is map-ish path.unshift(child._item.parentSub) } else { const parent = /** @type {import('../ynode.js').YNode} */ (child._item.parent) // parent is array-ish const apos = /** @type {import('../utils/RelativePosition.js').AbsolutePosition} */ (createAbsolutePositionFromRelativePosition(createRelativePosition(parent, child._item.id), doc, false, renderer)) path.unshift(apos.index) } child = /** @type {YNode} */ (child._item.parent) } return path }