-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #12664 from ckeditor/ck/12612-undo-ts
Other (undo): Rewrites ckeditor5-undo to TypeScript. Closes #12612.
- Loading branch information
Showing
18 changed files
with
737 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
/** | ||
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. | ||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license | ||
*/ | ||
|
||
/** | ||
* @module undo/basecommand | ||
*/ | ||
|
||
import Command from '@ckeditor/ckeditor5-core/src/command'; | ||
import { transformSets } from '@ckeditor/ckeditor5-engine/src/model/operation/transform'; | ||
import type { Editor } from '@ckeditor/ckeditor5-core'; | ||
import type { DataControllerSetEvent } from '@ckeditor/ckeditor5-engine/src/controller/datacontroller'; | ||
import type { Range } from '@ckeditor/ckeditor5-engine'; | ||
import type Batch from '@ckeditor/ckeditor5-engine/src/model/batch'; | ||
import type Operation from '@ckeditor/ckeditor5-engine/src/model/operation/operation'; | ||
|
||
/** | ||
* Base class for undo feature commands: {@link module:undo/undocommand~UndoCommand} and {@link module:undo/redocommand~RedoCommand}. | ||
* | ||
* @protected | ||
* @extends module:core/command~Command | ||
*/ | ||
export default abstract class BaseCommand extends Command { | ||
protected _stack: Array<{ batch: Batch; selection: { ranges: Array<Range>; isBackward: boolean } }>; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
public _createdBatches: WeakSet<Batch>; | ||
|
||
constructor( editor: Editor ) { | ||
super( editor ); | ||
|
||
/** | ||
* Stack of items stored by the command. These are pairs of: | ||
* | ||
* * {@link module:engine/model/batch~Batch batch} saved by the command, | ||
* * {@link module:engine/model/selection~Selection selection} state at the moment of saving the batch. | ||
* | ||
* @protected | ||
* @member {Array} #_stack | ||
*/ | ||
this._stack = []; | ||
|
||
/** | ||
* Stores all batches that were created by this command. | ||
* | ||
* @protected | ||
* @member {WeakSet.<module:engine/model/batch~Batch>} #_createdBatches | ||
*/ | ||
this._createdBatches = new WeakSet(); | ||
|
||
// Refresh state, so the command is inactive right after initialization. | ||
this.refresh(); | ||
|
||
// Set the transparent batch for the `editor.data.set()` call if the | ||
// batch type is not set already. | ||
this.listenTo<DataControllerSetEvent>( editor.data, 'set', ( evt, data ) => { | ||
// Create a shallow copy of the options to not change the original args. | ||
// And make sure that an object is assigned to data[ 1 ]. | ||
data[ 1 ] = { ...data[ 1 ] }; | ||
|
||
const options = data[ 1 ]; | ||
|
||
// If batch type is not set, default to non-undoable batch. | ||
if ( !options.batchType ) { | ||
options.batchType = { isUndoable: false }; | ||
} | ||
}, { priority: 'high' } ); | ||
|
||
// Clear the stack for the `transparent` batches. | ||
this.listenTo<DataControllerSetEvent>( editor.data, 'set', ( evt, data ) => { | ||
// We can assume that the object exists and it has a `batchType` property. | ||
// It was ensured with a higher priority listener before. | ||
const options = data[ 1 ]!; | ||
|
||
if ( !options.batchType!.isUndoable ) { | ||
this.clearStack(); | ||
} | ||
} ); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public override refresh(): void { | ||
this.isEnabled = this._stack.length > 0; | ||
} | ||
|
||
/** | ||
* Stores a batch in the command, together with the selection state of the {@link module:engine/model/document~Document document} | ||
* created by the editor which this command is registered to. | ||
* | ||
* @param {module:engine/model/batch~Batch} batch The batch to add. | ||
*/ | ||
public addBatch( batch: Batch ): void { | ||
const docSelection = this.editor.model.document.selection; | ||
|
||
const selection = { | ||
ranges: docSelection.hasOwnRange ? Array.from( docSelection.getRanges() ) : [], | ||
isBackward: docSelection.isBackward | ||
}; | ||
|
||
this._stack.push( { batch, selection } ); | ||
this.refresh(); | ||
} | ||
|
||
/** | ||
* Removes all items from the stack. | ||
*/ | ||
public clearStack(): void { | ||
this._stack = []; | ||
this.refresh(); | ||
} | ||
|
||
/** | ||
* Restores the {@link module:engine/model/document~Document#selection document selection} state after a batch was undone. | ||
* | ||
* @protected | ||
* @param {Array.<module:engine/model/range~Range>} ranges Ranges to be restored. | ||
* @param {Boolean} isBackward A flag describing whether the restored range was selected forward or backward. | ||
* @param {Array.<module:engine/model/operation/operation~Operation>} operations Operations which has been applied | ||
* since selection has been stored. | ||
*/ | ||
protected _restoreSelection( | ||
ranges: Array<Range>, | ||
isBackward: boolean, | ||
operations: Array<Operation> | ||
): void { | ||
const model = this.editor.model; | ||
const document = model.document; | ||
|
||
// This will keep the transformed selection ranges. | ||
const selectionRanges: Array<Range> = []; | ||
|
||
// Transform all ranges from the restored selection. | ||
const transformedRangeGroups = ranges.map( range => range.getTransformedByOperations( operations ) ); | ||
const allRanges = transformedRangeGroups.flat(); | ||
|
||
for ( const rangeGroup of transformedRangeGroups ) { | ||
// While transforming there could appear ranges that are contained by other ranges, we shall ignore them. | ||
const transformed = rangeGroup | ||
.filter( range => range.root != document.graveyard ) | ||
.filter( range => !isRangeContainedByAnyOtherRange( range, allRanges ) ); | ||
|
||
// All the transformed ranges ended up in graveyard. | ||
if ( !transformed.length ) { | ||
continue; | ||
} | ||
|
||
// After the range got transformed, we have an array of ranges. Some of those | ||
// ranges may be "touching" -- they can be next to each other and could be merged. | ||
normalizeRanges( transformed ); | ||
|
||
// For each `range` from `ranges`, we take only one transformed range. | ||
// This is because we want to prevent situation where single-range selection | ||
// got transformed to multi-range selection. | ||
selectionRanges.push( transformed[ 0 ] ); | ||
} | ||
|
||
// @if CK_DEBUG_ENGINE // console.log( `Restored selection by undo: ${ selectionRanges.join( ', ' ) }` ); | ||
|
||
// `selectionRanges` may be empty if all ranges ended up in graveyard. If that is the case, do not restore selection. | ||
if ( selectionRanges.length ) { | ||
model.change( writer => { | ||
writer.setSelection( selectionRanges, { backward: isBackward } ); | ||
} ); | ||
} | ||
} | ||
|
||
/** | ||
* Undoes a batch by reversing that batch, transforming reversed batch and finally applying it. | ||
* This is a helper method for {@link #execute}. | ||
* | ||
* @protected | ||
* @param {module:engine/model/batch~Batch} batchToUndo The batch to be undone. | ||
* @param {module:engine/model/batch~Batch} undoingBatch The batch that will contain undoing changes. | ||
*/ | ||
protected _undo( batchToUndo: Batch, undoingBatch: Batch ): void { | ||
const model = this.editor.model; | ||
const document = model.document; | ||
|
||
// All changes done by the command execution will be saved as one batch. | ||
this._createdBatches.add( undoingBatch ); | ||
|
||
const operationsToUndo = batchToUndo.operations.slice().filter( operation => operation.isDocumentOperation ); | ||
operationsToUndo.reverse(); | ||
|
||
// We will process each operation from `batchToUndo`, in reverse order. If there were operations A, B and C in undone batch, | ||
// we need to revert them in reverse order, so first C' (reversed C), then B', then A'. | ||
for ( const operationToUndo of operationsToUndo ) { | ||
const nextBaseVersion = operationToUndo.baseVersion! + 1; | ||
const historyOperations = Array.from( document.history.getOperations( nextBaseVersion ) ); | ||
|
||
const transformedSets = transformSets( | ||
[ operationToUndo.getReversed() ], | ||
historyOperations, | ||
{ | ||
useRelations: true, | ||
document: this.editor.model.document, | ||
padWithNoOps: false, | ||
forceWeakRemove: true | ||
} | ||
); | ||
|
||
const reversedOperations = transformedSets.operationsA; | ||
|
||
// After reversed operation has been transformed by all history operations, apply it. | ||
for ( const operation of reversedOperations ) { | ||
// Before applying, add the operation to the `undoingBatch`. | ||
undoingBatch.addOperation( operation ); | ||
model.applyOperation( operation ); | ||
|
||
document.history.setOperationAsUndone( operationToUndo, operation ); | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Normalizes list of ranges by joining intersecting or "touching" ranges. | ||
// | ||
// @param {Array.<module:engine/model/range~Range>} ranges | ||
// | ||
function normalizeRanges( ranges: Array<Range> ) { | ||
ranges.sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 ); | ||
|
||
for ( let i = 1; i < ranges.length; i++ ) { | ||
const previousRange = ranges[ i - 1 ]; | ||
const joinedRange = previousRange.getJoined( ranges[ i ], true ); | ||
|
||
if ( joinedRange ) { | ||
// Replace the ranges on the list with the new joined range. | ||
i--; | ||
ranges.splice( i, 2, joinedRange ); | ||
} | ||
} | ||
} | ||
|
||
function isRangeContainedByAnyOtherRange( range: Range, ranges: Array<Range> ) { | ||
return ranges.some( otherRange => otherRange !== range && otherRange.containsRange( range, true ) ); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
/** | ||
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. | ||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license | ||
*/ | ||
|
||
/** | ||
* @module undo | ||
*/ | ||
|
||
export { default as Undo } from './undo'; | ||
export { default as UndoEditing } from './undoediting'; | ||
export { default as UndoUi } from './undoui'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/** | ||
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved. | ||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license | ||
*/ | ||
|
||
/** | ||
* @module undo/redocommand | ||
*/ | ||
|
||
import BaseCommand from './basecommand'; | ||
|
||
/** | ||
* The redo command stores {@link module:engine/model/batch~Batch batches} that were used to undo a batch by | ||
* {@link module:undo/undocommand~UndoCommand}. It is able to redo a previously undone batch by reversing the undoing | ||
* batches created by `UndoCommand`. The reversed batch is transformed by all the batches from | ||
* {@link module:engine/model/document~Document#history history} that happened after the reversed undo batch. | ||
* | ||
* The redo command also takes care of restoring the {@link module:engine/model/document~Document#selection document selection}. | ||
* | ||
* @extends module:undo/basecommand~BaseCommand | ||
*/ | ||
export default class RedoCommand extends BaseCommand { | ||
/** | ||
* Executes the command. This method reverts the last {@link module:engine/model/batch~Batch batch} added to | ||
* the command's stack, applies the reverted and transformed version on the | ||
* {@link module:engine/model/document~Document document} and removes the batch from the stack. | ||
* Then, it restores the {@link module:engine/model/document~Document#selection document selection}. | ||
* | ||
* @fires execute | ||
*/ | ||
public override execute(): void { | ||
const item = this._stack.pop()!; | ||
const redoingBatch = this.editor.model.createBatch( { isUndo: true } ); | ||
|
||
// All changes have to be done in one `enqueueChange` callback so other listeners will not step between consecutive | ||
// operations, or won't do changes to the document before selection is properly restored. | ||
this.editor.model.enqueueChange( redoingBatch, () => { | ||
const lastOperation = item.batch.operations[ item.batch.operations.length - 1 ]; | ||
const nextBaseVersion = lastOperation.baseVersion! + 1; | ||
const operations = this.editor.model.document.history.getOperations( nextBaseVersion ); | ||
|
||
this._restoreSelection( item.selection.ranges, item.selection.isBackward, operations ); | ||
this._undo( item.batch, redoingBatch ); | ||
} ); | ||
|
||
this.refresh(); | ||
} | ||
} |
Oops, something went wrong.