UNPKG

@ckeditor/ckeditor5-engine

Version:

The editing engine of CKEditor 5 – the best browser-based rich text editor.

207 lines (206 loc) 8.8 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ import { CKEditorError } from '@ckeditor/ckeditor5-utils'; /** * @module engine/model/history */ /** * `History` keeps the track of all the operations applied to the {@link module:engine/model/document~ModelDocument document}. */ export class History { /** * Operations added to the history. */ _operations = []; /** * Holds an information which {@link module:engine/model/operation/operation~Operation operation} undoes which * {@link module:engine/model/operation/operation~Operation operation}. * * Keys of the map are "undoing operations", that is operations that undone some other operations. For each key, the * value is an operation that has been undone by the "undoing operation". */ _undoPairs = new Map(); /** * Holds all undone operations. */ _undoneOperations = new Set(); /** * A map that allows retrieving the operations fast based on the given base version. */ _baseVersionToOperationIndex = new Map(); /** * The history version. */ _version = 0; /** * The gap pairs kept in the <from,to> format. * * Anytime the `history.version` is set to a version larger than `history.version + 1`, * a new <lastHistoryVersion, newHistoryVersion> entry is added to the map. */ _gaps = new Map(); /** * The version of the last operation in the history. * * The history version is incremented automatically when a new operation is added to the history. * Setting the version manually should be done only in rare circumstances when a gap is planned * between history versions. When doing so, a gap will be created and the history will accept adding * an operation with base version equal to the new history version. */ get version() { return this._version; } set version(version) { // Store a gap if there are some operations already in the history and the // new version does not increment the latest one. if (this._operations.length && version > this._version + 1) { this._gaps.set(this._version, version); } this._version = version; } /** * The last history operation. */ get lastOperation() { return this._operations[this._operations.length - 1]; } /** * Adds an operation to the history and increments the history version. * * The operation's base version should be equal to the history version. Otherwise an error is thrown. */ addOperation(operation) { if (operation.baseVersion !== this.version) { /** * Only operations with matching versions can be added to the history. * * @error model-document-history-addoperation-incorrect-version * @param {module:engine/model/operation/operation~Operation} operation The current operation. * @param {number} historyVersion The current document history version. */ throw new CKEditorError('model-document-history-addoperation-incorrect-version', this, { operation, historyVersion: this.version }); } this._operations.push(operation); this._version++; this._baseVersionToOperationIndex.set(operation.baseVersion, this._operations.length - 1); } /** * Returns operations from the given range of operation base versions that were added to the history. * * Note that there may be gaps in operations base versions. * * @param fromBaseVersion Base version from which operations should be returned (inclusive). * @param toBaseVersion Base version up to which operations should be returned (exclusive). * @returns History operations for the given range, in chronological order. */ getOperations(fromBaseVersion, toBaseVersion = this.version) { // When there is no operation in the history, return an empty array. // After that we can be sure that `firstOperation`, `lastOperation` are not nullish. if (!this._operations.length) { return []; } const firstOperation = this._operations[0]; if (fromBaseVersion === undefined) { fromBaseVersion = firstOperation.baseVersion; } // Change exclusive `toBaseVersion` to inclusive, so it will refer to the actual index. // Thanks to that mapping from base versions to operation indexes are possible. let inclusiveTo = toBaseVersion - 1; // Check if "from" or "to" point to a gap between versions. // If yes, then change the incorrect position to the proper side of the gap. // Thanks to it, it will be possible to get index of the operation. for (const [gapFrom, gapTo] of this._gaps) { if (fromBaseVersion > gapFrom && fromBaseVersion < gapTo) { fromBaseVersion = gapTo; } if (inclusiveTo > gapFrom && inclusiveTo < gapTo) { inclusiveTo = gapFrom - 1; } } // If the whole range is outside of the operation versions, then return an empty array. if (inclusiveTo < firstOperation.baseVersion || fromBaseVersion > this.lastOperation.baseVersion) { return []; } let fromIndex = this._baseVersionToOperationIndex.get(fromBaseVersion); // If the range starts before the first operation, then use the first operation as the range's start. if (fromIndex === undefined) { fromIndex = 0; } let toIndex = this._baseVersionToOperationIndex.get(inclusiveTo); // If the range ends after the last operation, then use the last operation as the range's end. if (toIndex === undefined) { toIndex = this._operations.length - 1; } // Return the part of the history operations based on the calculated start index and end index. return this._operations.slice(fromIndex, // The `toIndex` should be included in the returned operations, so add `1`. toIndex + 1); } /** * Returns operation from the history that bases on given `baseVersion`. * * @param baseVersion Base version of the operation to get. * @returns Operation with given base version or `undefined` if there is no such operation in history. */ getOperation(baseVersion) { const operationIndex = this._baseVersionToOperationIndex.get(baseVersion); if (operationIndex === undefined) { return; } return this._operations[operationIndex]; } /** * Marks in history that one operation is an operation that is undoing the other operation. By marking operation this way, * history is keeping more context information about operations, which helps in operational transformation. * * @param undoneOperation Operation which is undone by `undoingOperation`. * @param undoingOperation Operation which undoes `undoneOperation`. */ setOperationAsUndone(undoneOperation, undoingOperation) { this._undoPairs.set(undoingOperation, undoneOperation); this._undoneOperations.add(undoneOperation); } /** * Checks whether given `operation` is undoing any other operation. * * @param operation Operation to check. * @returns `true` if given `operation` is undoing any other operation, `false` otherwise. */ isUndoingOperation(operation) { return this._undoPairs.has(operation); } /** * Checks whether given `operation` has been undone by any other operation. * * @param operation Operation to check. * @returns `true` if given `operation` has been undone any other operation, `false` otherwise. */ isUndoneOperation(operation) { return this._undoneOperations.has(operation); } /** * For given `undoingOperation`, returns the operation which has been undone by it. * * @returns Operation that has been undone by given `undoingOperation` or `undefined` * if given `undoingOperation` is not undoing any other operation. */ getUndoneOperation(undoingOperation) { return this._undoPairs.get(undoingOperation); } /** * Resets the history of operations. */ reset() { this._version = 0; this._undoPairs = new Map(); this._operations = []; this._undoneOperations = new Set(); this._gaps = new Map(); this._baseVersionToOperationIndex = new Map(); } }