8
0
Szymon Cofalik 9 лет назад
Родитель
Сommit
beb54f9be6
2 измененных файлов с 304 добавлено и 0 удалено
  1. 107 0
      packages/ckeditor5-undo/src/redocommand.js
  2. 197 0
      packages/ckeditor5-undo/tests/redocommand.js

+ 107 - 0
packages/ckeditor5-undo/src/redocommand.js

@@ -0,0 +1,107 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import BaseCommand from './basecommand.js';
+import { transformDelta as transformDelta } from './basecommand.js';
+
+/**
+ * Redo command stores {@link engine.model.Batch batches} that were used to undo a batch by {@link undo.UndoCommand UndoCommand}.
+ * It is able to redo a previously undone batch by reversing the undoing batches created by `UndoCommand`. Reversed batch is
+ * also transformed by batches from {@link engine.model.Document#history history} that happened after it and are not other redo batches.
+ *
+ * Redo command also takes care of restoring {@link engine.model.Document#selection selection} to the state before
+ * undone batch was applied.
+ *
+ * @memberOf undo
+ */
+export default class RedoCommand extends BaseCommand {
+	/**
+	 * Executes the command: reverts last {@link engine.model.Batch batch} added to the command's stack, applies
+	 * reverted and transformed version on the {@link engine.model.Document document} and removes the batch from the stack.
+	 * Then, restores {@link engine.model.Document#selection document selection}.
+	 *
+	 * @protected
+	 */
+	_doExecute() {
+		const item = this._items.pop();
+
+		// All changes done by the command execution will be saved as one batch.
+		const newBatch = this.editor.document.batch();
+		newBatch.type = 'redo';
+
+		// All changes have to be done in one `enqueueChanges` 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.document.enqueueChanges( () => {
+			this._redo( item.batch, newBatch, this.editor.document );
+			this._restoreSelection( item.selection.ranges, item.selection.isBackward );
+		} );
+
+		this.refreshState();
+	}
+
+	/**
+	 * Re-does a batch by reversing the batch that undone it, transforming that batch and applying it. This is
+	 * a helper method for {@link undo.RedoCommand#_doExecute}.
+	 *
+	 * @private
+	 * @param {engine.model.Batch} storedBatch Batch, which deltas will be reversed, transformed and applied.
+	 * @param {engine.model.Batch} redoingBatch Batch that will contain transformed and applied deltas from `storedBatch`.
+	 * @param {engine.model.Document} document Document that is operated on by the command.
+	 */
+	_redo( storedBatch, redoingBatch, document ) {
+		const deltasToRedo = storedBatch.deltas.slice();
+		deltasToRedo.reverse();
+
+		// We will process each delta from `storedBatch`, in reverse order. If there was deltas A, B and C in stored batch,
+		// we need to revert them in reverse order, so first reverse C, then B, then A.
+		for ( let deltaToRedo of deltasToRedo ) {
+			// Keep in mind that all algorithms return arrays. That's because the transformation might result in multiple
+			// deltas, so we need arrays to handle them anyway. To simplify algorithms, it is better to always have arrays
+			// in mind. For simplicity reasons, we will use singular form in descriptions and names.
+
+			const nextBaseVersion = deltaToRedo.baseVersion + deltaToRedo.operations.length;
+
+			// As stated above, convert delta to array of deltas.
+			let reversedDelta = [ deltaToRedo.getReversed() ];
+
+			// 1. Transform that delta by deltas from history that happened after it.
+			// Omit deltas from "redo" batches, because reversed delta already bases on them. Transforming by them
+			// again will result in incorrect deltas.
+			for ( let historyDelta of document.history.getDeltas( nextBaseVersion ) ) {
+				if ( historyDelta.batch.type != 'redo' ) {
+					reversedDelta = transformDelta( reversedDelta, [ historyDelta ], true );
+				}
+			}
+
+			// 2. After reversed delta has been transformed by all history deltas, apply it.
+			for ( let delta of reversedDelta ) {
+				// Fix base version.
+				delta.baseVersion = document.version;
+
+				// Before applying, add the delta to the `redoingBatch`.
+				redoingBatch.addDelta( delta );
+
+				// Now, apply all operations of the delta.
+				for ( let operation of delta.operations ) {
+					document.applyOperation( operation );
+				}
+			}
+		}
+	}
+
+	/**
+	 * Restores {@link engine.model.Document#selection document selection} state after a batch has been re-done. This
+	 * is a helper method for {@link undo.RedoCommand#_doExecute}.
+	 *
+	 * @private
+	 * @param {Array.<engine.model.Range>} ranges Ranges to be restored.
+	 * @param {Boolean} isBackward Flag describing if restored range was selected forward or backward.
+	 */
+	_restoreSelection( ranges, isBackward ) {
+		this.editor.document.selection.setRanges( ranges, isBackward );
+	}
+}

+ 197 - 0
packages/ckeditor5-undo/tests/redocommand.js

@@ -0,0 +1,197 @@
+/**
+ * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+'use strict';
+
+import ModelTestEditor from '/tests/ckeditor5/_utils/modeltesteditor.js';
+import Range from '/ckeditor5/engine/model/range.js';
+import Position from '/ckeditor5/engine/model/position.js';
+import UndoCommand from '/ckeditor5/undo/undocommand.js';
+import RedoCommand from '/ckeditor5/undo/redocommand.js';
+
+let editor, doc, root, redo, undo;
+
+beforeEach( () => {
+	editor = new ModelTestEditor();
+	redo = new RedoCommand( editor );
+
+	doc = editor.document;
+
+	root = doc.getRoot();
+} );
+
+afterEach( () => {
+	redo.destroy();
+} );
+
+describe( 'RedoCommand', () => {
+	describe( '_execute', () => {
+		const p = pos => new Position( root, [].concat( pos ) );
+		const r = ( a, b ) => new Range( p( a ), p( b ) );
+
+		let batch0, batch1, batch2;
+		let batches = new Set();
+
+		beforeEach( () => {
+			undo = new UndoCommand( editor );
+
+			// Simple integration with undo.
+			doc.on( 'change', ( evt, type, data, batch ) => {
+				if ( batch.type == 'undo' && !batches.has( batch ) ) {
+					redo.addBatch( batch );
+					batches.add( batch );
+				}
+			} );
+
+			/*
+			 [root]
+			 - {}
+			 */
+			editor.document.selection.setRanges( [ r( 0, 0 ) ] );
+			batch0 = doc.batch();
+			undo.addBatch( batch0 );
+			batch0.insert( p( 0 ), 'foobar' );
+			/*
+			 [root]
+			 - f
+			 - o
+			 - o
+			 - b
+			 - a
+			 - r{}
+			 */
+			// Let's make things spicy and this time, make a backward selection.
+			editor.document.selection.setRanges( [ r( 2, 4 ) ], true );
+			batch1 = doc.batch();
+			undo.addBatch( batch1 );
+			batch1.setAttr( 'key', 'value', r( 2, 4 ) );
+			/*
+			 [root]
+			 - f
+			 - o
+			 - {o (key: value)
+			 - b} (key: value)
+			 - a
+			 - r
+			 */
+			editor.document.selection.setRanges( [ r( 1, 3 ) ] );
+			batch2 = doc.batch();
+			undo.addBatch( batch2 );
+			batch2.move( r( 1, 3 ), p( 6 ) );
+			/*
+			 [root]
+			 - f
+			 - b (key: value)
+			 - a
+			 - r
+			 - {o
+			 - o} (key: value)
+			 */
+		} );
+
+		it( 'should redo batch undone by undo command', () => {
+			undo._execute( batch2 );
+
+			redo._execute();
+			// Should be back at original state:
+			/*
+			 [root]
+			 - f
+			 - b (key: value)
+			 - a
+			 - r
+			 - {o
+			 - o} (key: value)
+			 */
+			expect( Array.from( root._children._nodes.map( node => node.text ) ).join( '' ) ).to.equal( 'fbaroo' );
+			expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+			expect( editor.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
+			expect( editor.document.selection.isBackward ).to.be.false;
+		} );
+
+		it( 'should redo series of batches undone by undo command', () => {
+			undo._execute( batch2 );
+			undo._execute( batch1 );
+			undo._execute( batch0 );
+
+			redo._execute();
+			// Should be like after applying `batch0`:
+			/*
+			 [root]
+			 - f
+			 - o
+			 - {o
+			 - b}
+			 - a
+			 - r
+			 */
+			expect( Array.from( root._children._nodes.map( node => node.text ) ).join( '' ) ).to.equal( 'foobar' );
+			expect( root._children._nodes.find( node => node.hasAttribute( 'key' ) ) ).to.be.undefined;
+
+			expect( editor.document.selection.getRanges().next().value.isEqual( r( 2, 4 ) ) ).to.be.true;
+			expect( editor.document.selection.isBackward ).to.be.true;
+
+			redo._execute();
+			// Should be like after applying `batch1`:
+			/*
+			 [root]
+			 - f
+			 - {o
+			 - o} (key: value)
+			 - b (key: value)
+			 - a
+			 - r
+			 */
+			expect( Array.from( root._children._nodes.map( node => node.text ) ).join( '' ) ).to.equal( 'foobar' );
+			expect( root.getChild( 2 ).getAttribute( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 3 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+			expect( editor.document.selection.getRanges().next().value.isEqual( r( 1, 3 ) ) ).to.be.true;
+			expect( editor.document.selection.isBackward ).to.be.false;
+
+			redo._execute();
+			// Should be like after applying `batch2`:
+			/*
+			 [root]
+			 - f
+			 - b (key: value)
+			 - a
+			 - r
+			 - {o
+			 - o} (key: value)
+			 */
+			expect( Array.from( root._children._nodes.map( node => node.text ) ).join( '' ) ).to.equal( 'fbaroo' );
+			expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+			expect( editor.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
+			expect( editor.document.selection.isBackward ).to.be.false;
+		} );
+
+		it( 'should redo batch selectively undone by undo command', () => {
+			undo._execute( batch0 );
+			redo._execute();
+
+			// Should be back to original state:
+			/*
+			 [root]
+			 - f
+			 - b (key: value)
+			 - a
+			 - r
+			 - {o
+			 - o} (key: value)
+			 */
+			expect( Array.from( root._children._nodes.map( node => node.text ) ).join( '' ) ).to.equal( 'fbaroo' );
+			expect( root.getChild( 1 ).getAttribute( 'key' ) ).to.equal( 'value' );
+			expect( root.getChild( 5 ).getAttribute( 'key' ) ).to.equal( 'value' );
+
+			expect( editor.document.selection.getRanges().next().value.isEqual( r( 4, 6 ) ) ).to.be.true;
+			expect( editor.document.selection.isBackward ).to.be.false;
+		} );
+	} );
+} );