Skip to content

Commit

Permalink
Merge pull request #12664 from ckeditor/ck/12612-undo-ts
Browse files Browse the repository at this point in the history
Other (undo): Rewrites ckeditor5-undo to TypeScript. Closes #12612.
  • Loading branch information
arkflpc authored Oct 19, 2022
2 parents ccbaa6d + e0d47d8 commit dfe6323
Show file tree
Hide file tree
Showing 18 changed files with 737 additions and 4 deletions.
18 changes: 18 additions & 0 deletions packages/ckeditor5-engine/src/controller/datacontroller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,3 +639,21 @@ function _getMarkersRelativeToElement( element: ModelElement ): Map<string, Mode

return new Map( result );
}

export type DataControllerInitEvent = {
name: 'init';
args: [ Parameters<DataController[ 'init' ]> ];
return: ReturnType<DataController[ 'init' ]>;
};

export type DataControllerSetEvent = {
name: 'set';
args: [ Parameters<DataController[ 'set' ]> ];
return: ReturnType<DataController[ 'set' ]>;
};

export type DataControllerGetEvent = {
name: 'get';
args: [ Parameters<DataController[ 'get' ]> ];
return: ReturnType<DataController[ 'get' ]>;
};
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.
16 changes: 12 additions & 4 deletions packages/ckeditor5-undo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"ckeditor5-plugin",
"ckeditor5-dll"
],
"main": "src/index.js",
"main": "src/index.ts",
"dependencies": {
"@ckeditor/ckeditor5-core": "^35.2.1",
"@ckeditor/ckeditor5-engine": "^35.2.1",
Expand All @@ -25,7 +25,10 @@
"@ckeditor/ckeditor5-paragraph": "^35.2.1",
"@ckeditor/ckeditor5-typing": "^35.2.1",
"@ckeditor/ckeditor5-table": "^35.2.1",
"@ckeditor/ckeditor5-utils": "^35.2.1"
"@ckeditor/ckeditor5-utils": "^35.2.1",
"typescript": "^4.8.4",
"webpack": "^5.58.1",
"webpack-cli": "^4.9.0"
},
"engines": {
"node": ">=14.0.0",
Expand All @@ -42,9 +45,14 @@
},
"files": [
"lang",
"src",
"src/**/*.js",
"src/**/*.d.ts",
"theme",
"ckeditor5-metadata.json",
"CHANGELOG.md"
]
],
"scripts": {
"build": "tsc -p ./tsconfig.release.json",
"postversion": "npm run build"
}
}
242 changes: 242 additions & 0 deletions packages/ckeditor5-undo/src/basecommand.ts
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 ) );
}
12 changes: 12 additions & 0 deletions packages/ckeditor5-undo/src/index.ts
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';
48 changes: 48 additions & 0 deletions packages/ckeditor5-undo/src/redocommand.ts
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();
}
}
Loading

0 comments on commit dfe6323

Please sign in to comment.