Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Merge pull request #85 from ckeditor/t/84
Browse files Browse the repository at this point in the history
Fix: `UndoCommand` and `RedoCommand` should pass batch in `model.Model#enqueueChange` call. Closes #84.
  • Loading branch information
scofalik authored Mar 29, 2018
2 parents 99cee0e + 668fde6 commit 497af30
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 9 deletions.
7 changes: 2 additions & 5 deletions src/basecommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import Command from '@ckeditor/ckeditor5-core/src/command';
import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';

/**
* Base class for undo feature commands: {@link module:undo/undocommand~UndoCommand} and {@link module:undo/redocommand~RedoCommand}.
Expand Down Expand Up @@ -123,13 +122,13 @@ export default class BaseCommand extends Command {
*
* @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.
*/
_undo( batchToUndo ) {
_undo( batchToUndo, undoingBatch ) {
const model = this.editor.model;
const document = model.document;

// All changes done by the command execution will be saved as one batch.
const undoingBatch = new Batch();
this._createdBatches.add( undoingBatch );

const deltasToUndo = batchToUndo.deltas.slice();
Expand Down Expand Up @@ -168,8 +167,6 @@ export default class BaseCommand extends Command {
}
}
}

return undoingBatch;
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/redocommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import BaseCommand from './basecommand';
import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';

/**
* The redo command stores {@link module:engine/model/batch~Batch batches} that were used to undo a batch by
Expand All @@ -30,16 +31,17 @@ export default class RedoCommand extends BaseCommand {
*/
execute() {
const item = this._stack.pop();
const redoingBatch = new Batch();

// All changes have to be done in one `enqueueChange` callback so other listeners will not
// step between consecutive deltas, or won't do changes to the document before selection is properly restored.
this.editor.model.enqueueChange( () => {
this.editor.model.enqueueChange( redoingBatch, () => {
const lastDelta = item.batch.deltas[ item.batch.deltas.length - 1 ];
const nextBaseVersion = lastDelta.baseVersion + lastDelta.operations.length;
const deltas = this.editor.model.document.history.getDeltas( nextBaseVersion );

this._restoreSelection( item.selection.ranges, item.selection.isBackward, deltas );
this._undo( item.batch );
this._undo( item.batch, redoingBatch );
} );

this.refresh();
Expand Down
6 changes: 4 additions & 2 deletions src/undocommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import BaseCommand from './basecommand';
import Batch from '@ckeditor/ckeditor5-engine/src/model/batch';

/**
* The undo command stores {@link module:engine/model/batch~Batch batches} applied to the
Expand All @@ -33,11 +34,12 @@ export default class UndoCommand extends BaseCommand {
const batchIndex = batch ? this._stack.findIndex( a => a.batch == batch ) : this._stack.length - 1;

const item = this._stack.splice( batchIndex, 1 )[ 0 ];
const undoingBatch = new Batch();

// All changes has to be done in one `enqueueChange` callback so other listeners will not
// step between consecutive deltas, or won't do changes to the document before selection is properly restored.
this.editor.model.enqueueChange( () => {
const undoingBatch = this._undo( item.batch );
this.editor.model.enqueueChange( undoingBatch, () => {
this._undo( item.batch, undoingBatch );

const deltas = this.editor.model.document.history.getDeltas( item.batch.baseVersion );
this._restoreSelection( item.selection.ranges, item.selection.isBackward, deltas );
Expand Down
17 changes: 17 additions & 0 deletions tests/redocommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,23 @@ describe( 'RedoCommand', () => {
expect( editor.model.document.selection.getFirstRange().isEqual( r( 4, 6 ) ) ).to.be.true;
expect( editor.model.document.selection.isBackward ).to.be.false;
} );

it( 'should pass redoing batch to enqueueChange method', () => {
undo.execute( batch2 );

const enqueueChangeSpy = sinon.spy( model, 'enqueueChange' );
const undoSpy = sinon.spy( redo, '_undo' );

redo.execute();

sinon.assert.calledOnce( enqueueChangeSpy );
sinon.assert.calledOnce( undoSpy );

const redoingBatch = enqueueChangeSpy.firstCall.args[ 0 ];

expect( redoingBatch instanceof Batch ).to.be.true;
expect( undoSpy.firstCall.args[ 1 ] ).to.equal( redoingBatch );
} );
} );
} );
} );
15 changes: 15 additions & 0 deletions tests/undocommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,21 @@ describe( 'UndoCommand', () => {
expect( element.getAttribute( 'foo' ) ).to.equal( 'bar' );
expect( root.getAttribute( 'foo' ) ).to.not.equal( 'bar' );
} );

it( 'should pass undoing batch to enqueueChange method', () => {
const enqueueChangeSpy = sinon.spy( model, 'enqueueChange' );
const undoSpy = sinon.spy( undo, '_undo' );

undo.execute();

sinon.assert.calledOnce( enqueueChangeSpy );
sinon.assert.calledOnce( undoSpy );

const undoingBatch = enqueueChangeSpy.firstCall.args[ 0 ];

expect( undoingBatch instanceof Batch ).to.be.true;
expect( undoSpy.firstCall.args[ 1 ] ).to.equal( undoingBatch );
} );
} );

it( 'merges touching ranges when restoring selection', () => {
Expand Down
42 changes: 42 additions & 0 deletions tests/undoediting-integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ describe( 'UndoEditing integration', () => {
} );
} );

afterEach( () => {
return editor.destroy();
} );

function setSelection( pathA, pathB ) {
model.change( writer => {
writer.setSelection( new Range( new Position( root, pathA ), new Position( root, pathB ) ) );
Expand Down Expand Up @@ -1044,6 +1048,44 @@ describe( 'UndoEditing integration', () => {
} );
} );

it( 'postfixers should not add another undo step when fixing undo changes', () => {
input( '<paragraph>[]</paragraph>' );
const paragraph = root.getChild( 0 );

// 1. Step - add text and marker to the paragraph.
model.change( writer => {
writer.appendText( 'foo', paragraph );
writer.setMarker( 'marker', Range.createIn( paragraph ), { usingOperation: true } );
} );

// 2. Step - remove text from paragraph.
model.change( writer => {
writer.remove( Range.createIn( paragraph ) );
} );

// Register post fixer to remove marker from non-empty paragraph.
model.document.registerPostFixer( writer => {
const hasMarker = model.markers.has( 'marker' );
const isEmpty = paragraph.childCount === 0;

// Remove marker when paragraph is empty.
if ( hasMarker && !isEmpty ) {
writer.removeMarker( 'marker' );

return true;
}

return false;
} );

editor.execute( 'undo' );

// Check if postfixer changes are executed together with undo and there is no additional undo steps.
expect( model.markers.has( 'marker' ) ).to.be.false;
expect( editor.commands.get( 'undo' )._stack.length ).to.equal( 1 );
output( '<paragraph>foo[]</paragraph>' );
} );

function split( path ) {
setSelection( path.slice(), path.slice() );
editor.execute( 'enter' );
Expand Down

0 comments on commit 497af30

Please sign in to comment.