@ckeditor/ckeditor5-undo
Advanced tools
Comparing version 0.8.1 to 0.9.0
Changelog | ||
========= | ||
## [0.9.0](https://github.com/ckeditor/ckeditor5-undo/compare/v0.8.1...v0.9.0) (2017-09-03) | ||
### Other changes | ||
* Aligned the implementation to the new Command API (see https://github.com/ckeditor/ckeditor5-core/issues/88). ([a94dd46](https://github.com/ckeditor/ckeditor5-undo/commit/a94dd46)) | ||
* Cleaned up SVG icons. ([9cb1923](https://github.com/ckeditor/ckeditor5-undo/commit/9cb1923)) | ||
### BREAKING CHANGES | ||
* The command API has been changed. | ||
## [0.8.1](https://github.com/ckeditor/ckeditor5-undo/compare/v0.8.0...v0.8.1) (2017-05-07) | ||
@@ -5,0 +17,0 @@ |
@@ -6,3 +6,3 @@ /** | ||
/* jshint browser: false, node: true, strict: true */ | ||
/* eslint-env node */ | ||
@@ -12,3 +12,4 @@ 'use strict'; | ||
const gulp = require( 'gulp' ); | ||
const ckeditor5Lint = require( '@ckeditor/ckeditor5-dev-lint' )( { | ||
const ckeditor5Lint = require( '@ckeditor/ckeditor5-dev-lint' ); | ||
const options = { | ||
// Files ignored by `gulp lint` task. | ||
@@ -19,6 +20,6 @@ // Files from .gitignore will be added automatically during task execution. | ||
] | ||
} ); | ||
}; | ||
gulp.task( 'lint', ckeditor5Lint.lint ); | ||
gulp.task( 'lint-staged', ckeditor5Lint.lintStaged ); | ||
gulp.task( 'lint', () => ckeditor5Lint.lint( options ) ); | ||
gulp.task( 'lint-staged', () => ckeditor5Lint.lintStaged( options ) ); | ||
gulp.task( 'pre-commit', [ 'lint-staged' ] ); |
{ | ||
"name": "@ckeditor/ckeditor5-undo", | ||
"version": "0.8.1", | ||
"version": "0.9.0", | ||
"description": "Undo manager for CKEditor 5.", | ||
"keywords": [], | ||
"dependencies": { | ||
"@ckeditor/ckeditor5-core": "^0.8.1", | ||
"@ckeditor/ckeditor5-engine": "^0.10.0", | ||
"@ckeditor/ckeditor5-ui": "^0.9.0" | ||
"@ckeditor/ckeditor5-core": "^0.9.0", | ||
"@ckeditor/ckeditor5-engine": "^0.11.0", | ||
"@ckeditor/ckeditor5-ui": "^0.10.0" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-dev-lint": "^2.0.2", | ||
"@ckeditor/ckeditor5-basic-styles": "^0.8.1", | ||
"@ckeditor/ckeditor5-editor-classic": "^0.7.3", | ||
"@ckeditor/ckeditor5-enter": "^0.9.1", | ||
"@ckeditor/ckeditor5-heading": "^0.9.1", | ||
"@ckeditor/ckeditor5-paragraph": "^0.8.0", | ||
"@ckeditor/ckeditor5-typing": "^0.9.1", | ||
"@ckeditor/ckeditor5-utils": "^0.9.1", | ||
"gulp": "^3.9.0", | ||
"@ckeditor/ckeditor5-dev-lint": "^3.1.0", | ||
"@ckeditor/ckeditor5-basic-styles": "^0.9.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^0.8.0", | ||
"@ckeditor/ckeditor5-enter": "^0.10.0", | ||
"@ckeditor/ckeditor5-heading": "^0.10.0", | ||
"@ckeditor/ckeditor5-paragraph": "^0.9.0", | ||
"@ckeditor/ckeditor5-typing": "^0.10.0", | ||
"@ckeditor/ckeditor5-utils": "^0.10.0", | ||
"eslint-config-ckeditor5": "^1.0.5", | ||
"gulp": "^3.9.1", | ||
"guppy-pre-commit": "^0.4.0" | ||
@@ -22,0 +23,0 @@ }, |
@@ -1,4 +0,5 @@ | ||
CKEditor 5 Undo Manager | ||
CKEditor 5 undo feature | ||
======================================== | ||
[![Join the chat at https://gitter.im/ckeditor/ckeditor5](https://badges.gitter.im/ckeditor/ckeditor5.svg)](https://gitter.im/ckeditor/ckeditor5?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||
[![npm version](https://badge.fury.io/js/%40ckeditor%2Fckeditor5-undo.svg)](https://www.npmjs.com/package/@ckeditor/ckeditor5-undo) | ||
@@ -5,0 +6,0 @@ [![Build Status](https://travis-ci.org/ckeditor/ckeditor5-undo.svg?branch=master)](https://travis-ci.org/ckeditor/ckeditor5-undo) |
@@ -10,3 +10,3 @@ /** | ||
import Command from '@ckeditor/ckeditor5-core/src/command/command'; | ||
import Command from '@ckeditor/ckeditor5-core/src/command'; | ||
@@ -17,3 +17,3 @@ /** | ||
* @protected | ||
* @extends module:core/command/command~Command | ||
* @extends module:core/command~Command | ||
*/ | ||
@@ -43,7 +43,14 @@ export default class BaseCommand extends Command { | ||
// Refresh state, so command is inactive just after initialization. | ||
this.refreshState(); | ||
// Refresh state, so the command is inactive right after initialization. | ||
this.refresh(); | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
refresh() { | ||
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} | ||
@@ -61,3 +68,3 @@ * created by the editor which this command is registered to. | ||
this._stack.push( { batch, selection } ); | ||
this.refreshState(); | ||
this.refresh(); | ||
} | ||
@@ -70,13 +77,6 @@ | ||
this._stack = []; | ||
this.refreshState(); | ||
this.refresh(); | ||
} | ||
/** | ||
* @inheritDoc | ||
*/ | ||
_checkEnabled() { | ||
return this._stack.length > 0; | ||
} | ||
/** | ||
* Restores the {@link module:engine/model/document~Document#selection document selection} state after a batch was undone. | ||
@@ -87,2 +87,3 @@ * | ||
* @param {Boolean} isBackward A flag describing whether the restored range was selected forward or backward. | ||
* @param {Array.<module:engine/model/delta/delta~Delta>} deltas Deltas which has been applied since selection has been stored. | ||
*/ | ||
@@ -96,3 +97,3 @@ _restoreSelection( ranges, isBackward, deltas ) { | ||
// Transform all ranges from the restored selection. | ||
for ( let range of ranges ) { | ||
for ( const range of ranges ) { | ||
const transformedRanges = transformSelectionRange( range, deltas ); | ||
@@ -105,3 +106,3 @@ | ||
const transformedRange = transformedRanges.find( | ||
( range ) => range.start.root != document.graveyard | ||
range => range.start.root != document.graveyard | ||
); | ||
@@ -120,23 +121,70 @@ | ||
} | ||
/** | ||
* 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. | ||
*/ | ||
_undo( batchToUndo ) { | ||
const document = this.editor.document; | ||
// All changes done by the command execution will be saved as one batch. | ||
const undoingBatch = document.batch(); | ||
this._createdBatches.add( undoingBatch ); | ||
const deltasToUndo = batchToUndo.deltas.slice(); | ||
deltasToUndo.reverse(); | ||
// We will process each delta from `batchToUndo`, in reverse order. If there were deltas 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 deltaToUndo of deltasToUndo ) { | ||
// Keep in mind that transformation algorithms return arrays. That's because the transformation might result in multiple | ||
// deltas, so we need arrays to handle them. To simplify algorithms, it is better to always operate on arrays. | ||
const nextBaseVersion = deltaToUndo.baseVersion + deltaToUndo.operations.length; | ||
// Reverse delta from the history. | ||
const historyDeltas = Array.from( document.history.getDeltas( nextBaseVersion ) ); | ||
const transformedSets = document.transformDeltas( [ deltaToUndo.getReversed() ], historyDeltas, true ); | ||
const reversedDeltas = transformedSets.deltasA; | ||
// After reversed delta has been transformed by all history deltas, apply it. | ||
for ( const delta of reversedDeltas ) { | ||
// Fix base version. | ||
delta.baseVersion = document.version; | ||
// Before applying, add the delta to the `undoingBatch`. | ||
undoingBatch.addDelta( delta ); | ||
// Now, apply all operations of the delta. | ||
for ( const operation of delta.operations ) { | ||
document.applyOperation( operation ); | ||
} | ||
document.history.setDeltaAsUndone( deltaToUndo, delta ); | ||
} | ||
} | ||
return undoingBatch; | ||
} | ||
} | ||
// Transforms given range `range` by deltas from `document` history, starting from a delta with given `baseVersion`. | ||
// Transforms given range `range` by given `deltas`. | ||
// Returns an array containing one or more ranges, which are result of the transformation. | ||
function transformSelectionRange( range, deltas ) { | ||
// The range will be transformed by history deltas that happened after the selection got stored. | ||
// Note, that at this point, the document history is already updated by undo command execution. We will | ||
// not transform the range by deltas that got undone or their reversing counterparts. | ||
let transformed = transformRangesByDeltas( [ range ], deltas ); | ||
const transformed = transformRangesByDeltas( [ range ], deltas ); | ||
// After `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. | ||
// First, we have to sort those ranges because they don't have to be in an order. | ||
// First, we have to sort those ranges to assure that they are in order. | ||
transformed.sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 ); | ||
// Then, we check if two consecutive ranges are touching. | ||
for ( let i = 1 ; i < transformed.length; i++ ) { | ||
let a = transformed[ i - 1 ]; | ||
let b = transformed[ i ]; | ||
for ( let i = 1; i < transformed.length; i++ ) { | ||
const a = transformed[ i - 1 ]; | ||
const b = transformed[ i ]; | ||
if ( a.end.isTouching( b.start ) ) { | ||
// And join them together if they are. | ||
a.end = b.end; | ||
@@ -153,4 +201,4 @@ transformed.splice( i, 1 ); | ||
export function transformRangesByDeltas( ranges, deltas ) { | ||
for ( let delta of deltas ) { | ||
for ( let operation of delta.operations ) { | ||
for ( const delta of deltas ) { | ||
for ( const operation of delta.operations ) { | ||
// We look through all operations from all deltas. | ||
@@ -157,0 +205,0 @@ |
@@ -11,3 +11,2 @@ /** | ||
import BaseCommand from './basecommand'; | ||
import { transformDeltaSets } from '@ckeditor/ckeditor5-engine/src/model/delta/transform'; | ||
@@ -17,7 +16,6 @@ /** | ||
* {@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 also transformed by batches from | ||
* {@link module:engine/model/document~Document#history history} that happened after it and are not other redo batches. | ||
* 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} | ||
* to the state before an undone batch was applied. | ||
* The redo command also takes care of restoring the {@link module:engine/model/document~Document#selection document selection}. | ||
* | ||
@@ -33,5 +31,5 @@ * @extends module:undo/basecommand~BaseCommand | ||
* | ||
* @protected | ||
* @fires execute | ||
*/ | ||
_doExecute() { | ||
execute() { | ||
const item = this._stack.pop(); | ||
@@ -44,71 +42,10 @@ | ||
const nextBaseVersion = lastDelta.baseVersion + lastDelta.operations.length; | ||
const deltas = this.editor.document.history.getDeltas( nextBaseVersion ); | ||
// Selection state is from the moment after undo happened. It needs to be transformed by all the deltas | ||
// that happened after the selection state got saved. Unfortunately it is tricky, because those deltas | ||
// are already compressed in the history (they are removed). | ||
// Because of that we will transform the selection only by non-redo deltas | ||
const deltas = Array.from( this.editor.document.history.getDeltas( nextBaseVersion ) ).filter( ( delta ) => { | ||
return !this._createdBatches.has( delta.batch ); | ||
} ); | ||
this._restoreSelection( item.selection.ranges, item.selection.isBackward, deltas ); | ||
this._redo( item.batch ); | ||
this._undo( item.batch ); | ||
} ); | ||
this.refreshState(); | ||
this.refresh(); | ||
} | ||
/** | ||
* Redoes a batch by reversing the batch that has undone it, transforming that batch and applying it. This is | ||
* a helper method for {@link #_doExecute}. | ||
* | ||
* @private | ||
* @param {module:engine/model/batch~Batch} storedBatch The batch whose deltas will be reversed, transformed and applied. | ||
*/ | ||
_redo( storedBatch ) { | ||
const document = this.editor.document; | ||
// All changes done by the command execution will be saved as one batch. | ||
const redoingBatch = document.batch(); | ||
this._createdBatches.add( redoingBatch ); | ||
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 ( !this._createdBatches.has( historyDelta.batch ) ) { | ||
reversedDelta = transformDeltaSets( reversedDelta, [ historyDelta ], true ).deltasA; | ||
} | ||
} | ||
// 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 ); | ||
} | ||
} | ||
} | ||
} | ||
} |
120
src/undo.js
@@ -33,5 +33,5 @@ /** | ||
* =========== ================================== | ||
* [delta A1] [batch A with selection before A1] | ||
* [delta B1] [batch B with selection before B1] | ||
* [delta B2] [batch C with selection before C1] | ||
* [delta A1] [batch A] | ||
* [delta B1] [batch B] | ||
* [delta B2] [batch C] | ||
* [delta C1] | ||
@@ -54,77 +54,51 @@ * [delta C2] | ||
* History Undo stack | ||
* =========== ================================== | ||
* [delta A1 ] [batch A with selection before A1] | ||
* [delta B1 ] [batch B with selection before B1] | ||
* [delta B2 ] [ processing undoing batch C ] | ||
* [delta C1 ] | ||
* [delta C2 ] | ||
* [delta B3 ] | ||
* [delta C3 ] | ||
* [delta C3r] | ||
* ============= ================================== | ||
* [ delta A1 ] [ batch A ] | ||
* [ delta B1 ] [ batch B ] | ||
* [ delta B2 ] [ processing undoing batch C ] | ||
* [ delta C1 ] | ||
* [ delta C2 ] | ||
* [ delta B3 ] | ||
* [ delta C3 ] | ||
* [ delta C3r ] | ||
* | ||
* Next is delta `C2`, reversed to `C2r`. `C2r` bases on `C2`, so it bases on the wrong document state. It needs to be | ||
* transformed by deltas from history that happened after it, so it "knows" about them. Let us assume that `C2' = C2r * B3 * C3 * C3r`, | ||
* where `*` means "transformed by". As can be seen, `C2r` is transformed by a delta which is undone afterwards anyway. | ||
* This brings two problems: lower effectiveness (obvious) and incorrect results. Bad results come from the fact that | ||
* operational transformation algorithms assume there is no connection between two transformed operations when resolving | ||
* conflicts, which is true for example for collaborative editing, but is not true for the undo algorithm. | ||
* where `*` means "transformed by". Rest of deltas from that batch are processed in the same fashion. | ||
* | ||
* To prevent both problems, `History` introduces an API to {@link module:engine/model/history~History#removeDelta remove} | ||
* deltas from history. It is used to remove undone and undoing deltas after they are applied. It feels right — since when a | ||
* delta is undone or reversed, it is "removed" and there should be no sign of it in the history (fig. 1). | ||
* History Undo stack Redo stack | ||
* ============= ================================== ================================== | ||
* [ delta A1 ] [ batch A ] [ batch Cr ] | ||
* [ delta B1 ] [ batch B ] | ||
* [ delta B2 ] | ||
* [ delta C1 ] | ||
* [ delta C2 ] | ||
* [ delta B3 ] | ||
* [ delta C3 ] | ||
* [ delta C3r ] | ||
* [ delta C2' ] | ||
* [ delta C1' ] | ||
* | ||
* Notes: | ||
* Selective undo works on the same basis, however, instead of undoing the last batch in the undo stack, any batch can be undone. | ||
* The same algorithm applies: deltas from a batch (i.e. `A1`) are reversed and then transformed by deltas stored in history. | ||
* | ||
* * `---` symbolizes a removed delta. | ||
* * `'` symbolizes a reversed delta that was later transformed. | ||
* Redo also is very similar to undo. It has its own stack that is filled with undoing (reversed batches). Deltas from | ||
* batch that is re-done are reversed-back, transformed in proper order and applied to the document. | ||
* | ||
* History (fig. 1) History (fig. 2) History (fig. 3) | ||
* ================ ================ ================ | ||
* [delta A1] [delta A1] [delta A1] | ||
* [delta B1] [delta B1] [delta B1] | ||
* [delta B2] [delta B2] [delta B2] | ||
* [delta C1] [delta C1] [---C1---] | ||
* [delta C2] [---C2---] [---C2---] | ||
* [delta B3] [delta B3] [delta B3] | ||
* [---C3---] [---C3---] [---C3---] | ||
* [---C3r--] [---C3r--] [---C3r--] | ||
* [---C2'--] [---C2'--] | ||
* [---C1'--] | ||
* History Undo stack Redo stack | ||
* ============= ================================== ================================== | ||
* [ delta A1 ] [ batch A ] | ||
* [ delta B1 ] [ batch B ] | ||
* [ delta B2 ] [ batch Crr ] | ||
* [ delta C1 ] | ||
* [ delta C2 ] | ||
* [ delta B3 ] | ||
* [ delta C3 ] | ||
* [ delta C3r ] | ||
* [ delta C2' ] | ||
* [ delta C1' ] | ||
* [ delta C1'r] | ||
* [ delta C2'r] | ||
* [ delta C3rr] | ||
* | ||
* `C2r` can now be transformed only by `B3` and both `C2'` and `C2` can be removed (fig. 2). Same with `C1` (fig. 3). | ||
* | ||
* But what about that selection? For batch `C`, undo feature remembers the selection just before `C1` was applied. It can be | ||
* visualized between delta `B2` and `B3` (see fig. 3). As can be seen, some operations were applied to the document since the selection | ||
* state was remembered. Setting the document selection as it was remembered would be incorrect. It feels natural that | ||
* the selection state should also be transformed by deltas from history. The same pattern applies as with transforming deltas — | ||
* ranges should not be transformed by undone and undoing deltas. Thankfully, those deltas are already removed from history. | ||
* | ||
* Unfortunately, a problem appears with delta `B3`. It still remembers the context of deltas `C2` and `C1` on which it bases. | ||
* It is an obvious error — transforming by that delta would lead to incorrect results or "repeating" history would | ||
* produce a different document than the actual one. | ||
* | ||
* To prevent this situation, `B3` needs to also be {@link module:engine/model/history~History#updateDelta updated} in history. | ||
* It should be kept in a state that "does not remember" deltas that were removed from history. It is easily | ||
* achieved while transforming the reversed delta. For example, when `C2r` is transformed by `B3`, at the same time `B3` is | ||
* transformed by `C2r`. Transforming `B3` that remembers `C2` by a delta reversing `C2` effectively makes `B3` "forget" about `C2`. | ||
* By doing these transformations you effectively make `B3` base on `B2` which is the correct state of history (fig. 4). | ||
* | ||
* History (fig. 4) History (fig. 5) | ||
* =========================== =============================== | ||
* [delta A1] [---A1---] | ||
* [delta B1] [delta B1 "without A1"] | ||
* [delta B2] [delta B2 "without A1"] | ||
* [---C1---] [---C1---] | ||
* [---C2---] [---C2---] | ||
* [delta B3 "without C2, C1"] [delta B3 "without C2, C1, A1"] | ||
* [---C3---] [---C3---] | ||
* [---C3r--] [---C3r--] | ||
* [---C2'--] [---C2'--] | ||
* [---C1'--] [---C1'--] | ||
* [---A1'--] | ||
* | ||
* Selective undo works on the same basis, however, instead of undoing the last batch in the undo stack, any batch can be undone. | ||
* The same algorithm applies: deltas from a batch (i.e. `A1`) are reversed and then transformed by deltas stored in history, | ||
* simultaneously updating them. Then deltas are applied to the document and removed from history (fig. 5). | ||
* | ||
* @extends module:core/plugin~Plugin | ||
@@ -144,3 +118,3 @@ */ | ||
static get pluginName() { | ||
return 'undo/undo'; | ||
return 'Undo'; | ||
} | ||
@@ -176,7 +150,7 @@ | ||
editor.ui.componentFactory.add( name, ( locale ) => { | ||
editor.ui.componentFactory.add( name, locale => { | ||
const view = new ButtonView( locale ); | ||
view.set( { | ||
label: label, | ||
label, | ||
icon: Icon, | ||
@@ -183,0 +157,0 @@ keystroke, |
@@ -11,4 +11,2 @@ /** | ||
import BaseCommand from './basecommand'; | ||
import { transformRangesByDeltas } from './basecommand'; | ||
import { transformDeltaSets } from '@ckeditor/ckeditor5-engine/src/model/delta/transform'; | ||
@@ -18,6 +16,5 @@ /** | ||
* {@link module:engine/model/document~Document document} and is able to undo a batch by reversing it and transforming by | ||
* other batches from {@link module:engine/model/document~Document#history history} that happened after the reversed batch. | ||
* batches from {@link module:engine/model/document~Document#history history} that happened after the reversed batch. | ||
* | ||
* The undo command also takes care of restoring the {@link module:engine/model/document~Document#selection document selection} | ||
* to the state before the undone batch was applied. | ||
* The undo command also takes care of restoring the {@link module:engine/model/document~Document#selection document selection}. | ||
* | ||
@@ -32,9 +29,9 @@ * @extends module:undo/basecommand~BaseCommand | ||
* | ||
* @protected | ||
* @fires execute | ||
* @fires revert | ||
* @param {module:engine/model/batch~Batch} [batch] A batch that should be undone. If not set, the last added batch will be undone. | ||
*/ | ||
_doExecute( batch = null ) { | ||
execute( batch = null ) { | ||
// If batch is not given, set `batchIndex` to the last index in command stack. | ||
let batchIndex = batch ? this._stack.findIndex( ( a ) => a.batch == batch ) : this._stack.length - 1; | ||
const batchIndex = batch ? this._stack.findIndex( a => a.batch == batch ) : this._stack.length - 1; | ||
@@ -54,134 +51,4 @@ const item = this._stack.splice( batchIndex, 1 )[ 0 ]; | ||
this.refreshState(); | ||
this.refresh(); | ||
} | ||
/** | ||
* Returns an index in {@link module:undo/basecommand~BaseCommand#_stack} pointing to the item that is storing a | ||
* batch that has a given {@link module:engine/model/batch~Batch#baseVersion}. | ||
* | ||
* @private | ||
* @param {Number} baseVersion The base version of the batch to find. | ||
* @returns {Number|null} | ||
*/ | ||
_getItemIndexFromBaseVersion( baseVersion ) { | ||
for ( let i = 0; i < this._stack.length; i++ ) { | ||
if ( this._stack[ i ].batch.baseVersion == baseVersion ) { | ||
return i; | ||
} | ||
} | ||
return null; | ||
} | ||
/** | ||
* Undoes a batch by reversing a batch from history, transforming that reversed batch and applying it. This is | ||
* a helper method for {@link #_doExecute}. | ||
* | ||
* @private | ||
* @param {module:engine/model/batch~Batch} batchToUndo A batch whose deltas will be reversed, transformed and applied. | ||
*/ | ||
_undo( batchToUndo ) { | ||
const document = this.editor.document; | ||
// All changes done by the command execution will be saved as one batch. | ||
const undoingBatch = document.batch(); | ||
this._createdBatches.add( undoingBatch ); | ||
const history = document.history; | ||
const deltasToUndo = batchToUndo.deltas.slice(); | ||
deltasToUndo.reverse(); | ||
// We will process each delta from `batchToUndo`, in reverse order. If there was deltas A, B and C in undone batch, | ||
// we need to revert them in reverse order, so first reverse C, then B, then A. | ||
for ( let deltaToUndo of deltasToUndo ) { | ||
// 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 baseVersion = deltaToUndo.baseVersion; | ||
const nextBaseVersion = baseVersion + deltaToUndo.operations.length; | ||
// 1. Get updated version of the delta from the history. | ||
// Batch stored in the undo command might have an outdated version of the delta that should be undone. | ||
// To prevent errors, we will take an updated version of it from the history, basing on delta's `baseVersion`. | ||
const updatedDeltaToUndo = history.getDelta( baseVersion ); | ||
// This is a safe valve in case of not finding delta to undo in history. This may come up if that delta | ||
// got updated into no deltas, or removed from history. | ||
if ( updatedDeltaToUndo === null ) { | ||
continue; | ||
} | ||
// 2. Reverse delta from the history. | ||
updatedDeltaToUndo.reverse(); | ||
let reversedDelta = []; | ||
for ( let delta of updatedDeltaToUndo ) { | ||
reversedDelta.push( delta.getReversed() ); | ||
} | ||
// Stores history deltas transformed by `deltaToUndo`. Will be used later for updating document history. | ||
const updatedHistoryDeltas = {}; | ||
// 3. Transform reversed delta by history deltas that happened after delta to undo. We have to bring | ||
// reversed delta to the current state of document. While doing this, we will also update history deltas | ||
// to the state which "does not remember" delta that we undo. | ||
for ( let historyDelta of history.getDeltas( nextBaseVersion ) ) { | ||
// 3.1. Transform selection range stored with history batch by reversed delta. | ||
// It is important to keep stored selection ranges updated. As we are removing and updating deltas in the history, | ||
// selection ranges would base on outdated history state. | ||
const itemIndex = this._getItemIndexFromBaseVersion( historyDelta.baseVersion ); | ||
// `itemIndex` will be `null` for `historyDelta` if it is not the first delta in it's batch. | ||
// This is fine, because we want to transform each selection only once, before transforming reversed delta | ||
// by the first delta of the batch connected with the ranges. | ||
if ( itemIndex !== null ) { | ||
this._stack[ itemIndex ].selection.ranges = transformRangesByDeltas( this._stack[ itemIndex ].selection.ranges, reversedDelta ); | ||
} | ||
// 3.2. Transform reversed delta by history delta and vice-versa. | ||
const results = transformDeltaSets( reversedDelta, [ historyDelta ], true ); | ||
reversedDelta = results.deltasA; | ||
const updatedHistoryDelta = results.deltasB; | ||
// 3.3. Store updated history delta. Later, it will be updated in `history`. | ||
if ( !updatedHistoryDeltas[ historyDelta.baseVersion ] ) { | ||
updatedHistoryDeltas[ historyDelta.baseVersion ] = []; | ||
} | ||
updatedHistoryDeltas[ historyDelta.baseVersion ] = updatedHistoryDeltas[ historyDelta.baseVersion ].concat( updatedHistoryDelta ); | ||
} | ||
// 4. 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 `undoingBatch`. | ||
undoingBatch.addDelta( delta ); | ||
// Now, apply all operations of the delta. | ||
for ( let operation of delta.operations ) { | ||
document.applyOperation( operation ); | ||
} | ||
} | ||
// 5. Remove reversed delta from the history. | ||
history.removeDelta( baseVersion ); | ||
// And all deltas that are reversing it. | ||
// So the history looks like both original and reversing deltas never happened. | ||
// That's why we have to update history deltas - some of them might have been basing on deltas that we are now removing. | ||
for ( let delta of reversedDelta ) { | ||
history.removeDelta( delta.baseVersion ); | ||
} | ||
// 6. Update history deltas in history. | ||
for ( let historyBaseVersion in updatedHistoryDeltas ) { | ||
history.updateDelta( Number( historyBaseVersion ), updatedHistoryDeltas[ historyBaseVersion ] ); | ||
} | ||
} | ||
return undoingBatch; | ||
} | ||
} | ||
@@ -188,0 +55,0 @@ |
@@ -63,4 +63,4 @@ /** | ||
// Register command to the editor. | ||
this.editor.commands.set( 'undo', this._undoCommand ); | ||
this.editor.commands.set( 'redo', this._redoCommand ); | ||
this.editor.commands.add( 'undo', this._undoCommand ); | ||
this.editor.commands.add( 'redo', this._redoCommand ); | ||
@@ -67,0 +67,0 @@ this.listenTo( this.editor.document, 'change', ( evt, type, changes, batch ) => { |
@@ -10,3 +10,3 @@ /** | ||
describe( 'BaseCommand', () => { | ||
let editor, doc, root, base; | ||
let editor, doc, base; | ||
@@ -18,4 +18,2 @@ beforeEach( () => { | ||
doc = editor.document; | ||
root = doc.getRoot(); | ||
} ); | ||
@@ -29,15 +27,15 @@ | ||
it( 'should create command with empty batch stack', () => { | ||
expect( base._checkEnabled() ).to.be.false; | ||
expect( base.isEnabled ).to.be.false; | ||
} ); | ||
} ); | ||
describe( '_checkEnabled', () => { | ||
it( 'should return false if there are no batches in command stack', () => { | ||
expect( base._checkEnabled() ).to.be.false; | ||
describe( 'isEnabled', () => { | ||
it( 'should be false if there are no batches in command stack', () => { | ||
expect( base.isEnabled ).to.be.false; | ||
} ); | ||
it( 'should return true if there are batches in command stack', () => { | ||
it( 'should be true if there are batches in command stack', () => { | ||
base.addBatch( doc.batch() ); | ||
expect( base._checkEnabled() ).to.be.true; | ||
expect( base.isEnabled ).to.be.true; | ||
} ); | ||
@@ -51,5 +49,5 @@ } ); | ||
expect( base._checkEnabled() ).to.be.false; | ||
expect( base.isEnabled ).to.be.false; | ||
} ); | ||
} ); | ||
} ); |
@@ -8,3 +8,3 @@ /** | ||
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classic'; | ||
import ClassicEditor from '@ckeditor/ckeditor5-editor-classic/src/classiceditor'; | ||
import Enter from '@ckeditor/ckeditor5-enter/src/enter'; | ||
@@ -18,11 +18,12 @@ import Typing from '@ckeditor/ckeditor5-typing/src/typing'; | ||
ClassicEditor.create( document.querySelector( '#editor' ), { | ||
plugins: [ Enter, Typing, Paragraph, Heading, Undo, Bold, Italic ], | ||
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ] | ||
} ) | ||
.then( editor => { | ||
window.editor = editor; | ||
} ) | ||
.catch( err => { | ||
console.error( err.stack ); | ||
} ); | ||
ClassicEditor | ||
.create( document.querySelector( '#editor' ), { | ||
plugins: [ Enter, Typing, Paragraph, Heading, Undo, Bold, Italic ], | ||
toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ] | ||
} ) | ||
.then( editor => { | ||
window.editor = editor; | ||
} ) | ||
.catch( err => { | ||
console.error( err.stack ); | ||
} ); |
@@ -26,7 +26,7 @@ /** | ||
afterEach( () => { | ||
redo.destroy(); | ||
return editor.destroy(); | ||
} ); | ||
describe( 'RedoCommand', () => { | ||
describe( '_execute', () => { | ||
describe( 'execute()', () => { | ||
const p = pos => new Position( root, [].concat( pos ) ); | ||
@@ -36,3 +36,3 @@ const r = ( a, b ) => new Range( p( a ), p( b ) ); | ||
let batch0, batch1, batch2; | ||
let batches = new Set(); | ||
const batches = new Set(); | ||
@@ -58,2 +58,3 @@ beforeEach( () => { | ||
batch0.insert( p( 0 ), 'foobar' ); | ||
/* | ||
@@ -73,2 +74,3 @@ [root] | ||
batch1.setAttribute( r( 2, 4 ), 'key', 'value' ); | ||
/* | ||
@@ -87,2 +89,3 @@ [root] | ||
batch2.move( r( 1, 3 ), p( 6 ) ); | ||
/* | ||
@@ -100,5 +103,5 @@ [root] | ||
it( 'should redo batch undone by undo command', () => { | ||
undo._execute( batch2 ); | ||
undo.execute( batch2 ); | ||
redo._execute(); | ||
redo.execute(); | ||
// Should be back at original state: | ||
@@ -123,7 +126,7 @@ /* | ||
it( 'should redo series of batches undone by undo command', () => { | ||
undo._execute(); | ||
undo._execute(); | ||
undo._execute(); | ||
undo.execute(); | ||
undo.execute(); | ||
undo.execute(); | ||
redo._execute(); | ||
redo.execute(); | ||
// Should be like after applying `batch0`: | ||
@@ -145,3 +148,3 @@ /* | ||
redo._execute(); | ||
redo.execute(); | ||
// Should be like after applying `batch1`: | ||
@@ -164,3 +167,3 @@ /* | ||
redo._execute(); | ||
redo.execute(); | ||
// Should be like after applying `batch2`: | ||
@@ -185,4 +188,4 @@ /* | ||
it( 'should redo batch selectively undone by undo command', () => { | ||
undo._execute( batch0 ); | ||
redo._execute(); | ||
undo.execute( batch0 ); | ||
redo.execute(); | ||
@@ -208,6 +211,6 @@ // Should be back to original state: | ||
it( 'should redo batch selectively undone by undo command #2', () => { | ||
undo._execute( batch1 ); | ||
undo._execute( batch2 ); | ||
redo._execute(); | ||
redo._execute(); | ||
undo.execute( batch1 ); | ||
undo.execute( batch2 ); | ||
redo.execute(); | ||
redo.execute(); | ||
@@ -235,3 +238,3 @@ // Should be back to original state: | ||
// Undo moving "oo" to the end of string. Now it is "foOBar". Capitals mean set attribute. | ||
undo._execute(); | ||
undo.execute(); | ||
@@ -242,3 +245,3 @@ // Remove "ar". | ||
// Undo setting attribute on "ob". Now it is "foob". | ||
undo._execute(); | ||
undo.execute(); | ||
@@ -249,3 +252,3 @@ // Append "xx" at the beginning. Now it is "xxfoob". | ||
// Redo setting attribute on "ob". Now it is "xxfoOB". | ||
redo._execute(); | ||
redo.execute(); | ||
@@ -259,3 +262,3 @@ expect( getText( root ) ).to.equal( 'xxfoob' ); | ||
// Redo moving "oo". Now it is "xxfBoO". Selection is expected to be on just moved "oO". | ||
redo._execute(); | ||
redo.execute(); | ||
@@ -262,0 +265,0 @@ expect( getText( root ) ).to.equal( 'xxfboo' ); |
@@ -24,5 +24,3 @@ /** | ||
return ClassicTestEditor.create( editorElement, { | ||
plugins: [ Undo ] | ||
} ) | ||
return ClassicTestEditor.create( editorElement, { plugins: [ Undo ] } ) | ||
.then( newEditor => { | ||
@@ -53,3 +51,8 @@ editor = newEditor; | ||
const wasHandled = editor.keystrokes.press( { keyCode: keyCodes.z, ctrlKey: true } ); | ||
const wasHandled = editor.keystrokes.press( { | ||
keyCode: keyCodes.z, | ||
ctrlKey: true, | ||
preventDefault: sinon.spy(), | ||
stopPropagation: sinon.spy() | ||
} ); | ||
@@ -63,3 +66,8 @@ expect( wasHandled ).to.be.true; | ||
const wasHandled = editor.keystrokes.press( { keyCode: keyCodes.y, ctrlKey: true } ); | ||
const wasHandled = editor.keystrokes.press( { | ||
keyCode: keyCodes.y, | ||
ctrlKey: true, | ||
preventDefault: sinon.spy(), | ||
stopPropagation: sinon.spy() | ||
} ); | ||
@@ -72,7 +80,15 @@ expect( wasHandled ).to.be.true; | ||
const spy = sinon.stub( editor, 'execute' ); | ||
const keyEventData = { | ||
keyCode: keyCodes.z, | ||
ctrlKey: true, | ||
shiftKey: true, | ||
preventDefault: sinon.spy(), | ||
stopPropagation: sinon.spy() | ||
}; | ||
const wasHandled = editor.keystrokes.press( { keyCode: keyCodes.z, ctrlKey: true, shiftKey: true } ); | ||
const wasHandled = editor.keystrokes.press( keyEventData ); | ||
expect( wasHandled ).to.be.true; | ||
expect( spy.calledWithExactly( 'redo' ) ).to.be.true; | ||
expect( keyEventData.preventDefault.calledOnce ).to.be.true; | ||
} ); | ||
@@ -79,0 +95,0 @@ |
@@ -11,3 +11,2 @@ /** | ||
import UndoCommand from '../src/undocommand'; | ||
import AttributeDelta from '@ckeditor/ckeditor5-engine/src/model/delta/attributedelta'; | ||
import { itemAt, getText } from '@ckeditor/ckeditor5-engine/tests/model/_utils/utils'; | ||
@@ -28,3 +27,3 @@ | ||
afterEach( () => { | ||
undo.destroy(); | ||
return editor.destroy(); | ||
} ); | ||
@@ -36,3 +35,3 @@ | ||
describe( '_execute', () => { | ||
describe( 'execute()', () => { | ||
let batch0, batch1, batch2, batch3; | ||
@@ -49,2 +48,3 @@ | ||
batch0.insert( p( 0 ), 'foobar' ); | ||
/* | ||
@@ -64,2 +64,3 @@ [root] | ||
batch1.setAttribute( r( 2, 4 ), 'key', 'value' ); | ||
/* | ||
@@ -78,2 +79,3 @@ [root] | ||
batch2.move( r( 1, 3 ), p( 6 ) ); | ||
/* | ||
@@ -92,2 +94,3 @@ [root] | ||
batch3.wrap( r( 1, 4 ), 'p' ); | ||
/* | ||
@@ -105,2 +108,3 @@ [root] | ||
batch2.move( r( 0, 1 ), p( 3 ) ); | ||
/* | ||
@@ -120,3 +124,3 @@ [root] | ||
it( 'should revert changes done by deltas from the batch that was most recently added to the command stack', () => { | ||
undo._execute(); | ||
undo.execute(); | ||
@@ -141,3 +145,3 @@ // Selection is restored. Wrap is removed: | ||
undo._execute(); | ||
undo.execute(); | ||
@@ -159,6 +163,8 @@ // Two moves are removed: | ||
expect( editor.document.selection.getFirstRange().isEqual( r( 1, 3 ) ) ).to.be.true; | ||
// Since selection restoring is not 100% accurate, selected range is not perfectly correct | ||
// with what is expected in comment above. The correct result would be if range was [ 1 ] - [ 3 ]. | ||
expect( editor.document.selection.getFirstRange().isEqual( r( 0, 3 ) ) ).to.be.true; | ||
expect( editor.document.selection.isBackward ).to.be.false; | ||
undo._execute(); | ||
undo.execute(); | ||
@@ -183,3 +189,3 @@ // Set attribute is undone: | ||
undo._execute(); | ||
undo.execute(); | ||
@@ -196,3 +202,3 @@ // Insert is undone: | ||
it( 'should revert changes done by deltas from given batch, if parameter was passed (test: revert set attribute)', () => { | ||
undo._execute( batch1 ); | ||
undo.execute( batch1 ); | ||
// Remove attribute: | ||
@@ -225,3 +231,3 @@ /* | ||
it( 'should revert changes done by deltas from given batch, if parameter was passed (test: revert insert foobar)', () => { | ||
undo._execute( batch0 ); | ||
undo.execute( batch0 ); | ||
// Remove foobar: | ||
@@ -242,3 +248,3 @@ /* | ||
undo._execute( batch1 ); | ||
undo.execute( batch1 ); | ||
// Remove attributes. | ||
@@ -255,13 +261,12 @@ // This does nothing in the `root` because attributes were set on nodes that already got removed. | ||
expect( doc.graveyard.getChild( 0 ).maxOffset ).to.equal( 6 ); | ||
expect( doc.graveyard.maxOffset ).to.equal( 6 ); | ||
for ( let char of doc.graveyard._children ) { | ||
for ( const char of doc.graveyard._children ) { | ||
expect( char.hasAttribute( 'key' ) ).to.be.false; | ||
} | ||
// Let's undo wrapping. This should leave us with empty root. | ||
undo._execute( batch3 ); | ||
// Let's undo wrapping. This will remove the P element and leave us with empty root. | ||
undo.execute( batch3 ); | ||
expect( root.maxOffset ).to.equal( 0 ); | ||
// Once again transformed range ends up in the graveyard. | ||
expect( editor.document.selection.getFirstRange().isEqual( r( 0, 0 ) ) ).to.be.true; | ||
@@ -272,4 +277,3 @@ expect( editor.document.selection.isBackward ).to.be.false; | ||
// Some tests to ensure 100% CC and proper behavior in edge cases. | ||
describe( 'edge cases', () => { | ||
it( 'merges touching ranges when restoring selection', () => { | ||
function getCaseText( root ) { | ||
@@ -279,3 +283,3 @@ let text = ''; | ||
for ( let i = 0; i < root.childCount; i++ ) { | ||
let node = root.getChild( i ); | ||
const node = root.getChild( i ); | ||
text += node.getAttribute( 'uppercase' ) ? node.data.toUpperCase() : node.data; | ||
@@ -287,101 +291,25 @@ } | ||
it( 'correctly handles deltas in compressed history that were earlier updated into multiple deltas (or split when undoing)', () => { | ||
// In this case we assume that one of the deltas in compressed history was updated to two deltas. | ||
// This is a tricky edge case because it is almost impossible to come up with convincing scenario that produces it. | ||
// At the moment of writing this test and comment, only Undo feature uses `CompressedHistory#updateDelta`. | ||
// Because deltas that "stays" in history are transformed with `isStrong` flag set to `false`, `MoveOperation` | ||
// won't get split and `AttributeDelta` can hold multiple `AttributeOperation` in it. So using most common deltas | ||
// (`InsertDelta`, `RemoveDelta`, `MoveDelta`, `AttributeDelta`) and undo it's impossible to get to this edge case. | ||
// Still there might be some weird scenarios connected with OT / Undo / Collaborative Editing / other deltas / | ||
// fancy 3rd party plugin where it may come up, so it's better to be safe than sorry. | ||
root.appendChildren( new Text( 'abcdef' ) ); | ||
expect( getCaseText( root ) ).to.equal( 'abcdef' ); | ||
root.appendChildren( new Text( 'abcdef' ) ); | ||
expect( getCaseText( root ) ).to.equal( 'abcdef' ); | ||
editor.document.selection.setRanges( [ r( 1, 4 ) ] ); | ||
const batch0 = doc.batch(); | ||
undo.addBatch( batch0 ); | ||
batch0.setAttribute( r( 1, 4 ), 'uppercase', true ); | ||
expect( getCaseText( root ) ).to.equal( 'aBCDef' ); | ||
editor.document.selection.setRanges( [ r( 1, 4 ) ] ); | ||
let batch0 = doc.batch(); | ||
undo.addBatch( batch0 ); | ||
batch0.move( r( 1, 4 ), p( 5 ) ); | ||
expect( getCaseText( root ) ).to.equal( 'aebcdf' ); | ||
editor.document.selection.setRanges( [ r( 3, 4 ) ] ); | ||
const batch1 = doc.batch(); | ||
undo.addBatch( batch1 ); | ||
batch1.move( r( 3, 4 ), p( 1 ) ); | ||
expect( getCaseText( root ) ).to.equal( 'aDBCef' ); | ||
editor.document.selection.setRanges( [ r( 1, 1 ) ] ); | ||
let batch1 = doc.batch(); | ||
undo.addBatch( batch1 ); | ||
batch1.remove( r( 0, 1 ) ); | ||
expect( getCaseText( root ) ).to.equal( 'ebcdf' ); | ||
undo.execute( batch0 ); | ||
editor.document.selection.setRanges( [ r( 0, 3 ) ] ); | ||
let batch2 = doc.batch(); | ||
undo.addBatch( batch2 ); | ||
batch2.setAttribute( r( 0, 3 ), 'uppercase', true ); | ||
expect( getCaseText( root ) ).to.equal( 'EBCdf' ); | ||
undo._execute( batch0 ); | ||
expect( getCaseText( root ) ).to.equal( 'BCdEf' ); | ||
// Let's simulate splitting the delta by updating the history by hand. | ||
let attrHistoryDelta = doc.history.getDelta( 2 )[ 0 ]; | ||
let attrDelta1 = new AttributeDelta(); | ||
attrDelta1.addOperation( attrHistoryDelta.operations[ 0 ] ); | ||
let attrDelta2 = new AttributeDelta(); | ||
attrDelta2.addOperation( attrHistoryDelta.operations[ 1 ] ); | ||
doc.history.updateDelta( 2, [ attrDelta1, attrDelta2 ] ); | ||
undo._execute( batch1 ); | ||
// After this execution, undo algorithm should update both `attrDelta1` and `attrDelta2` with new | ||
// versions, that have incremented offsets. | ||
expect( getCaseText( root ) ).to.equal( 'aBCdEf' ); | ||
undo._execute( batch2 ); | ||
// This execution checks whether undo algorithm correctly updated deltas in previous execution | ||
// and also whether it correctly "reads" both deltas from history. | ||
expect( getCaseText( root ) ).to.equal( 'abcdef' ); | ||
} ); | ||
it( 'merges touching ranges when restoring selection', () => { | ||
root.appendChildren( new Text( 'abcdef' ) ); | ||
expect( getCaseText( root ) ).to.equal( 'abcdef' ); | ||
editor.document.selection.setRanges( [ r( 1, 4 ) ] ); | ||
let batch0 = doc.batch(); | ||
undo.addBatch( batch0 ); | ||
batch0.setAttribute( r( 1, 4 ), 'uppercase', true ); | ||
expect( getCaseText( root ) ).to.equal( 'aBCDef' ); | ||
editor.document.selection.setRanges( [ r( 3, 4 ) ] ); | ||
let batch1 = doc.batch(); | ||
undo.addBatch( batch1 ); | ||
batch1.move( r( 3, 4 ), p( 1 ) ); | ||
expect( getCaseText( root ) ).to.equal( 'aDBCef' ); | ||
undo._execute( batch0 ); | ||
// After undo-attr: acdbef <--- "cdb" should be selected, it would look weird if only "cd" or "b" is selected | ||
// but the whole unbroken part "cdb" changed attribute. | ||
expect( getCaseText( root ) ).to.equal( 'adbcef' ); | ||
expect( editor.document.selection.getFirstRange().isEqual( r( 1, 4 ) ) ).to.be.true; | ||
} ); | ||
it( 'does nothing (and not crashes) if delta to undo is no longer in history', () => { | ||
// Also an edgy situation but it may come up if other plugins use `CompressedHistory` API. | ||
root.appendChildren( new Text( 'abcdef' ) ); | ||
expect( getCaseText( root ) ).to.equal( 'abcdef' ); | ||
editor.document.selection.setRanges( [ r( 0, 1 ) ] ); | ||
let batch0 = doc.batch(); | ||
undo.addBatch( batch0 ); | ||
batch0.setAttribute( r( 0, 1 ), 'uppercase', true ); | ||
expect( getCaseText( root ) ).to.equal( 'Abcdef' ); | ||
doc.history.removeDelta( 0 ); | ||
root.getChild( 0 ).removeAttribute( 'uppercase' ); | ||
expect( getCaseText( root ) ).to.equal( 'abcdef' ); | ||
undo._execute(); | ||
// Nothing happened. We are still alive. | ||
expect( getCaseText( root ) ).to.equal( 'abcdef' ); | ||
} ); | ||
// After undo-attr: acdbef <--- "cdb" should be selected, it would look weird if only "cd" or "b" is selected | ||
// but the whole unbroken part "cdb" changed attribute. | ||
expect( getCaseText( root ) ).to.equal( 'adbcef' ); | ||
expect( editor.document.selection.getFirstRange().isEqual( r( 1, 4 ) ) ).to.be.true; | ||
} ); | ||
} ); | ||
} ); |
@@ -12,2 +12,7 @@ /** | ||
import DeleteCommand from '@ckeditor/ckeditor5-typing/src/deletecommand'; | ||
import InputCommand from '@ckeditor/ckeditor5-typing/src/inputcommand'; | ||
import EnterCommand from '@ckeditor/ckeditor5-enter/src/entercommand'; | ||
import AttributeCommand from '@ckeditor/ckeditor5-basic-styles/src/attributecommand'; | ||
import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; | ||
@@ -19,9 +24,20 @@ | ||
beforeEach( () => { | ||
return ModelTestEditor.create( { | ||
plugins: [ UndoEngine ] | ||
} ) | ||
return ModelTestEditor.create( { plugins: [ UndoEngine ] } ) | ||
.then( newEditor => { | ||
editor = newEditor; | ||
editor.commands.add( 'delete', new DeleteCommand( editor, 'backward' ) ); | ||
editor.commands.add( 'forwardDelete', new DeleteCommand( editor, 'forward' ) ); | ||
editor.commands.add( 'enter', new EnterCommand( editor ) ); | ||
editor.commands.add( 'input', new InputCommand( editor, 5 ) ); | ||
editor.commands.add( 'bold', new AttributeCommand( editor, 'bold' ) ); | ||
doc = editor.document; | ||
doc.schema.registerItem( 'p', '$block' ); | ||
doc.schema.registerItem( 'h1', '$block' ); | ||
doc.schema.registerItem( 'h2', '$block' ); | ||
doc.schema.allow( { name: '$inline', attributes: 'bold', inside: '$block' } ); | ||
doc.schema.registerItem( 'div', '$block' ); | ||
root = doc.getRoot(); | ||
@@ -47,310 +63,773 @@ } ); | ||
describe( 'UndoEngine integration', () => { | ||
describe( 'adding and removing content', () => { | ||
it( 'add and undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
function redoDisabled() { | ||
expect( editor.commands.get( 'redo' ).isEnabled ).to.be.false; | ||
} | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
describe( 'adding and removing content', () => { | ||
it( 'add and undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
it( 'multiple adding and undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
doc.batch() | ||
.insert( doc.selection.getFirstPosition(), 'zzz' ) | ||
.insert( new Position( root, [ 1, 0 ] ), 'xxx' ); | ||
output( '<p>fozzz[]o</p><p>xxxbar</p>' ); | ||
it( 'multiple adding and undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
setSelection( [ 1, 0 ], [ 1, 0 ] ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' ); | ||
output( '<p>fozzzo</p><p>yyy[]xxxbar</p>' ); | ||
doc.batch() | ||
.insert( doc.selection.getFirstPosition(), 'zzz' ) | ||
.insert( new Position( root, [ 1, 0 ] ), 'xxx' ); | ||
output( '<p>fozzz[]o</p><p>xxxbar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fozzzo</p><p>[]xxxbar</p>' ); | ||
setSelection( [ 1, 0 ], [ 1, 0 ] ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' ); | ||
output( '<p>fozzzo</p><p>yyy[]xxxbar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fozzzo</p><p>[]xxxbar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
it( 'multiple adding mixed with undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
it( 'multiple adding mixed with undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
setSelection( [ 1, 0 ], [ 1, 0 ] ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' ); | ||
output( '<p>fozzzo</p><p>yyy[]bar</p>' ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fozzzo</p><p>[]bar</p>' ); | ||
setSelection( [ 1, 0 ], [ 1, 0 ] ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'yyy' ); | ||
output( '<p>fozzzo</p><p>yyy[]bar</p>' ); | ||
setSelection( [ 0, 0 ], [ 0, 0 ] ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' ); | ||
output( '<p>xxx[]fozzzo</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fozzzo</p><p>[]bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>[]fozzzo</p><p>bar</p>' ); | ||
setSelection( [ 0, 0 ], [ 0, 0 ] ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'xxx' ); | ||
output( '<p>xxx[]fozzzo</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>[]fozzzo</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
it( 'multiple remove and undo', () => { | ||
input( '<p>[]foo</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) ); | ||
output( '<p>[]o</p><p>bar</p>' ); | ||
it( 'multiple remove and undo', () => { | ||
input( '<p>[]foo</p><p>bar</p>' ); | ||
setSelection( [ 1, 1 ], [ 1, 1 ] ); | ||
doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) ); | ||
output( '<p>o</p><p>b[]</p>' ); | ||
doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) ); | ||
output( '<p>[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
// Here is an edge case that selection could be before or after `ar`. | ||
output( '<p>o</p><p>b[]ar</p>' ); | ||
setSelection( [ 1, 1 ], [ 1, 1 ] ); | ||
doc.batch().remove( Range.createFromPositionAndShift( doc.selection.getFirstPosition(), 2 ) ); | ||
output( '<p>o</p><p>b[]</p>' ); | ||
editor.execute( 'undo' ); | ||
// As above. | ||
output( '<p>[]foo</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
// Here is an edge case that selection could be before or after `ar`. | ||
output( '<p>o</p><p>bar[]</p>' ); | ||
undoDisabled(); | ||
} ); | ||
editor.execute( 'undo' ); | ||
// As above. | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
it( 'add and remove different parts and undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
it( 'add and remove different parts and undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
setSelection( [ 1, 2 ], [ 1, 2 ] ); | ||
doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ) , 1 ) ); | ||
output( '<p>fozzzo</p><p>b[]r</p>' ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fozzzo</p><p>ba[]r</p>' ); | ||
setSelection( [ 1, 2 ], [ 1, 2 ] ); | ||
doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 1, 1 ] ), 1 ) ); | ||
output( '<p>fozzzo</p><p>b[]r</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fozzzo</p><p>ba[]r</p>' ); | ||
undoDisabled(); | ||
} ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
it( 'add and remove same part and undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
it( 'add and remove same part and undo', () => { | ||
input( '<p>fo[]o</p><p>bar</p>' ); | ||
doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ) , 3 ) ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
doc.batch().remove( Range.createFromPositionAndShift( new Position( root, [ 0, 2 ] ), 3 ) ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fozzz[]o</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[]o</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
} ); | ||
describe( 'moving', () => { | ||
it( 'move same content twice then undo', () => { | ||
input( '<p>f[o]z</p><p>bar</p>' ); | ||
describe( 'moving', () => { | ||
it( 'move same content twice then undo', () => { | ||
input( '<p>f[o]z</p><p>bar</p>' ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) ); | ||
output( '<p>fz</p><p>[o]bar</p>' ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) ); | ||
output( '<p>fz</p><p>[o]bar</p>' ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) ); | ||
output( '<p>fz[o]</p><p>bar</p>' ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0, 2 ] ) ); | ||
output( '<p>fz[o]</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fz</p><p>[o]bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fz</p><p>[o]bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>f[o]z</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>f[o]z</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
undoDisabled(); | ||
} ); | ||
it( 'move content and new parent then undo', () => { | ||
input( '<p>f[o]z</p><p>bar</p>' ); | ||
it( 'move content and new parent then undo', () => { | ||
input( '<p>f[o]z</p><p>bar</p>' ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) ); | ||
output( '<p>fz</p><p>[o]bar</p>' ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 1, 0 ] ) ); | ||
output( '<p>fz</p><p>[o]bar</p>' ); | ||
setSelection( [ 1 ], [ 2 ] ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) ); | ||
output( '[<p>obar</p>]<p>fz</p>' ); | ||
setSelection( [ 1 ], [ 2 ] ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) ); | ||
output( '[<p>obar</p>]<p>fz</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fz</p>[<p>obar</p>]' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fz</p>[<p>obar</p>]' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>f[o]z</p><p>bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>f[o]z</p><p>bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
undoDisabled(); | ||
} ); | ||
} ); | ||
describe( 'attributes with other', () => { | ||
it( 'attributes then insert inside then undo', () => { | ||
input( '<p>fo[ob]ar</p>' ); | ||
describe( 'attributes with other', () => { | ||
it( 'attributes then insert inside then undo', () => { | ||
input( '<p>fo[ob]ar</p>' ); | ||
doc.batch().setAttribute( doc.selection.getFirstRange(), 'bold', true ); | ||
output( '<p>fo[<$text bold="true">ob</$text>]ar</p>' ); | ||
doc.batch().setAttribute( doc.selection.getFirstRange(), 'bold', true ); | ||
output( '<p>fo[<$text bold="true">ob</$text>]ar</p>' ); | ||
setSelection( [ 0, 3 ], [ 0, 3 ] ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fo<$text bold="true">o</$text>zzz<$text bold="true">[]b</$text>ar</p>' ); | ||
expect( doc.selection.getAttribute( 'bold' ) ).to.true; | ||
setSelection( [ 0, 3 ], [ 0, 3 ] ); | ||
doc.batch().insert( doc.selection.getFirstPosition(), 'zzz' ); | ||
output( '<p>fo<$text bold="true">o</$text>zzz<$text bold="true">[]b</$text>ar</p>' ); | ||
expect( doc.selection.getAttribute( 'bold' ) ).to.true; | ||
editor.execute( 'undo' ); | ||
output( '<p>fo<$text bold="true">o[]b</$text>ar</p>' ); | ||
expect( doc.selection.getAttribute( 'bold' ) ).to.true; | ||
editor.execute( 'undo' ); | ||
output( '<p>fo<$text bold="true">o[]b</$text>ar</p>' ); | ||
expect( doc.selection.getAttribute( 'bold' ) ).to.true; | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[ob]ar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[ob]ar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
undoDisabled(); | ||
} ); | ||
} ); | ||
describe( 'wrapping, unwrapping, merging, splitting', () => { | ||
it( 'wrap and undo', () => { | ||
doc.schema.allow( { name: '$text', inside: '$root' } ); | ||
input( 'fo[zb]ar' ); | ||
describe( 'wrapping, unwrapping, merging, splitting', () => { | ||
it( 'wrap and undo', () => { | ||
doc.schema.allow( { name: '$text', inside: '$root' } ); | ||
input( 'fo[zb]ar' ); | ||
doc.batch().wrap( doc.selection.getFirstRange(), 'p' ); | ||
output( 'fo<p>[zb]</p>ar' ); | ||
doc.batch().wrap( doc.selection.getFirstRange(), 'p' ); | ||
output( 'fo<p>[zb]</p>ar' ); | ||
editor.execute( 'undo' ); | ||
output( 'fo[zb]ar' ); | ||
editor.execute( 'undo' ); | ||
output( 'fo[zb]ar' ); | ||
undoDisabled(); | ||
} ); | ||
undoDisabled(); | ||
} ); | ||
it( 'wrap, move and undo', () => { | ||
doc.schema.allow( { name: '$text', inside: '$root' } ); | ||
input( 'fo[zb]ar' ); | ||
it( 'wrap, move and undo', () => { | ||
doc.schema.allow( { name: '$text', inside: '$root' } ); | ||
input( 'fo[zb]ar' ); | ||
doc.batch().wrap( doc.selection.getFirstRange(), 'p' ); | ||
// Would be better if selection was inside P. | ||
output( 'fo<p>[zb]</p>ar' ); | ||
doc.batch().wrap( doc.selection.getFirstRange(), 'p' ); | ||
// Would be better if selection was inside P. | ||
output( 'fo<p>[zb]</p>ar' ); | ||
setSelection( [ 2, 0 ], [ 2, 1 ] ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) ); | ||
output( '[z]fo<p>b</p>ar' ); | ||
setSelection( [ 2, 0 ], [ 2, 1 ] ); | ||
doc.batch().move( doc.selection.getFirstRange(), new Position( root, [ 0 ] ) ); | ||
output( '[z]fo<p>b</p>ar' ); | ||
editor.execute( 'undo' ); | ||
output( 'fo<p>[z]b</p>ar' ); | ||
editor.execute( 'undo' ); | ||
output( 'fo<p>[z]b</p>ar' ); | ||
editor.execute( 'undo' ); | ||
output( 'fo[zb]ar' ); | ||
editor.execute( 'undo' ); | ||
output( 'fo[zb]ar' ); | ||
undoDisabled(); | ||
} ); | ||
undoDisabled(); | ||
} ); | ||
it( 'unwrap and undo', () => { | ||
input( '<p>foo[]bar</p>' ); | ||
it( 'unwrap and undo', () => { | ||
input( '<p>foo[]bar</p>' ); | ||
doc.batch().unwrap( doc.selection.getFirstPosition().parent ); | ||
output( 'foo[]bar' ); | ||
doc.batch().unwrap( doc.selection.getFirstPosition().parent ); | ||
output( 'foo[]bar' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>foo[]bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>foo[]bar</p>' ); | ||
undoDisabled(); | ||
} ); | ||
undoDisabled(); | ||
} ); | ||
it( 'merge and undo', () => { | ||
input( '<p>foo</p><p>[]bar</p>' ); | ||
it( 'merge and undo', () => { | ||
input( '<p>foo</p><p>[]bar</p>' ); | ||
doc.batch().merge( new Position( root, [ 1 ] ) ); | ||
// Because selection is stuck with <p> it ends up in graveyard. We have to manually move it to correct node. | ||
setSelection( [ 0, 3 ], [ 0, 3 ] ); | ||
output( '<p>foo[]bar</p>' ); | ||
doc.batch().merge( new Position( root, [ 1 ] ) ); | ||
// Because selection is stuck with <p> it ends up in graveyard. We have to manually move it to correct node. | ||
setSelection( [ 0, 3 ], [ 0, 3 ] ); | ||
output( '<p>foo[]bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>foo</p><p>[]bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>foo</p><p>bar[]</p>' ); | ||
undoDisabled(); | ||
} ); | ||
undoDisabled(); | ||
} ); | ||
it( 'split and undo', () => { | ||
input( '<p>foo[]bar</p>' ); | ||
it( 'split and undo', () => { | ||
input( '<p>foo[]bar</p>' ); | ||
doc.batch().split( doc.selection.getFirstPosition() ); | ||
// Because selection is stuck with <p> it ends up in wrong node. We have to manually move it to correct node. | ||
setSelection( [ 1, 0 ], [ 1, 0 ] ); | ||
output( '<p>foo</p><p>[]bar</p>' ); | ||
doc.batch().split( doc.selection.getFirstPosition() ); | ||
// Because selection is stuck with <p> it ends up in wrong node. We have to manually move it to correct node. | ||
setSelection( [ 1, 0 ], [ 1, 0 ] ); | ||
output( '<p>foo</p><p>[]bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>foo[]bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>foobar[]</p>' ); | ||
undoDisabled(); | ||
} ); | ||
undoDisabled(); | ||
} ); | ||
} ); | ||
describe( 'other edge cases', () => { | ||
it( 'deleteContent between two nodes', () => { | ||
input( '<p>fo[o</p><p>b]ar</p>' ); | ||
// Restoring selection in those examples may be completely off. | ||
describe( 'multiple enters, deletes and typing', () => { | ||
function split( path ) { | ||
setSelection( path.slice(), path.slice() ); | ||
editor.execute( 'enter' ); | ||
} | ||
editor.data.deleteContent( doc.selection, doc.batch(), { merge: true } ); | ||
output( '<p>fo[]ar</p>' ); | ||
function merge( path ) { | ||
const selPath = path.slice(); | ||
selPath.push( 0 ); | ||
setSelection( selPath, selPath.slice() ); | ||
editor.execute( 'delete' ); | ||
} | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[o</p><p>b]ar</p>' ); | ||
} ); | ||
function type( path, text ) { | ||
setSelection( path.slice(), path.slice() ); | ||
editor.execute( 'input', { text } ); | ||
} | ||
// Related to ckeditor5-engine#891 and ckeditor5-list#51. | ||
it( 'change attribute of removed node then undo and redo', () => { | ||
const gy = doc.graveyard; | ||
const batch = doc.batch(); | ||
const p = new Element( 'p' ); | ||
function remove( path ) { | ||
setSelection( path.slice(), path.slice() ); | ||
editor.execute( 'delete' ); | ||
} | ||
root.appendChildren( p ); | ||
it( 'split, split, split', () => { | ||
input( '<p>12345678</p>' ); | ||
batch.remove( p ); | ||
batch.setAttribute( p, 'bold', true ); | ||
split( [ 0, 3 ] ); | ||
output( '<p>123</p><p>[]45678</p>' ); | ||
editor.execute( 'undo' ); | ||
editor.execute( 'redo' ); | ||
split( [ 1, 4 ] ); | ||
output( '<p>123</p><p>4567</p><p>[]8</p>' ); | ||
expect( p.root ).to.equal( gy ); | ||
expect( p.getAttribute( 'bold' ) ).to.be.true; | ||
} ); | ||
split( [ 1, 2 ] ); | ||
output( '<p>123</p><p>45</p><p>[]67</p><p>8</p>' ); | ||
// Related to ckeditor5-engine#891. | ||
it( 'change attribute of removed node then undo and redo', () => { | ||
const gy = doc.graveyard; | ||
const batch = doc.batch(); | ||
const p1 = new Element( 'p' ); | ||
const p2 = new Element( 'p' ); | ||
const p3 = new Element( 'p' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>4567[]</p><p>8</p>' ); | ||
root.appendChildren( [ p1, p2 ] ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>45678[]</p>' ); | ||
batch.remove( p1 ).remove( p2 ).insert( new Position( root, [ 0 ] ), p3 ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345678[]</p>' ); | ||
editor.execute( 'undo' ); | ||
editor.execute( 'redo' ); | ||
undoDisabled(); | ||
expect( p1.root ).to.equal( gy ); | ||
expect( p2.root ).to.equal( gy ); | ||
expect( p3.root ).to.equal( root ); | ||
} ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123[]</p><p>45678</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123</p><p>4567[]</p><p>8</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123</p><p>45[]</p><p>67</p><p>8</p>' ); | ||
redoDisabled(); | ||
} ); | ||
it( 'merge, merge, merge', () => { | ||
input( '<p>123</p><p>45</p><p>67</p><p>8</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>123[]45</p><p>67</p><p>8</p>' ); | ||
merge( [ 2 ] ); | ||
output( '<p>12345</p><p>67[]8</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>12345[]678</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345</p><p>678[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345</p><p>67</p><p>8[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>45[]</p><p>67</p><p>8</p>' ); | ||
undoDisabled(); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345[]</p><p>67</p><p>8</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345</p><p>678[]</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345678[]</p>' ); | ||
redoDisabled(); | ||
} ); | ||
it( 'split, merge, split, merge (same position)', () => { | ||
input( '<p>12345678</p>' ); | ||
split( [ 0, 3 ] ); | ||
output( '<p>123</p><p>[]45678</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>123[]45678</p>' ); | ||
split( [ 0, 3 ] ); | ||
output( '<p>123</p><p>[]45678</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>123[]45678</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>45678[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345678[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>45678[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345678[]</p>' ); | ||
undoDisabled(); | ||
editor.execute( 'redo' ); | ||
output( '<p>123[]</p><p>45678</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345678[]</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123[]</p><p>45678</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345678[]</p>' ); | ||
redoDisabled(); | ||
} ); | ||
it( 'split, split, split, merge, merge, merge', () => { | ||
input( '<p>12345678</p>' ); | ||
split( [ 0, 3 ] ); | ||
output( '<p>123</p><p>[]45678</p>' ); | ||
split( [ 1, 4 ] ); | ||
output( '<p>123</p><p>4567</p><p>[]8</p>' ); | ||
split( [ 1, 2 ] ); | ||
output( '<p>123</p><p>45</p><p>[]67</p><p>8</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>123[]45</p><p>67</p><p>8</p>' ); | ||
merge( [ 2 ] ); | ||
output( '<p>12345</p><p>67[]8</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>12345[]678</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345</p><p>678[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345</p><p>67</p><p>8[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>45[]</p><p>67</p><p>8</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>4567[]</p><p>8</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>45678[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345678[]</p>' ); | ||
undoDisabled(); | ||
editor.execute( 'redo' ); | ||
output( '<p>123[]</p><p>45678</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123</p><p>4567[]</p><p>8</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123</p><p>45[]</p><p>67</p><p>8</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345[]</p><p>67</p><p>8</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345</p><p>678[]</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345678[]</p>' ); | ||
redoDisabled(); | ||
} ); | ||
it( 'split, split, merge, split, merge (different order)', () => { | ||
input( '<p>12345678</p>' ); | ||
split( [ 0, 3 ] ); | ||
output( '<p>123</p><p>[]45678</p>' ); | ||
split( [ 1, 2 ] ); | ||
output( '<p>123</p><p>45</p><p>[]678</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>123[]45</p><p>678</p>' ); | ||
split( [ 1, 1 ] ); | ||
output( '<p>12345</p><p>6</p><p>[]78</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>12345[]6</p><p>78</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345</p><p>6[]</p><p>78</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345</p><p>678[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>45[]</p><p>678</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>45678[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345678[]</p>' ); | ||
undoDisabled(); | ||
editor.execute( 'redo' ); | ||
output( '<p>123[]</p><p>45678</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123</p><p>45[]</p><p>678</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345[]</p><p>678</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345</p><p>6[]</p><p>78</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123456[]</p><p>78</p>' ); | ||
redoDisabled(); | ||
} ); | ||
it( 'split, remove, split, merge, merge', () => { | ||
input( '<p>12345678</p>' ); | ||
split( [ 0, 3 ] ); | ||
output( '<p>123</p><p>[]45678</p>' ); | ||
remove( [ 1, 4 ] ); | ||
remove( [ 1, 3 ] ); | ||
output( '<p>123</p><p>45[]8</p>' ); | ||
split( [ 1, 1 ] ); | ||
output( '<p>123</p><p>4</p><p>[]58</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>123[]4</p><p>58</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>1234[]58</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>1234</p><p>58[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>4[]</p><p>58</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>458[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>4567[]8</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345678[]</p>' ); | ||
undoDisabled(); | ||
editor.execute( 'redo' ); | ||
output( '<p>123[]</p><p>45678</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123</p><p>458[]</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123</p><p>4[]</p><p>58</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>1234[]</p><p>58</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123458[]</p>' ); | ||
redoDisabled(); | ||
} ); | ||
it( 'split, typing, split, merge, merge', () => { | ||
input( '<p>12345678</p>' ); | ||
split( [ 0, 3 ] ); | ||
output( '<p>123</p><p>[]45678</p>' ); | ||
type( [ 1, 4 ], 'x' ); | ||
type( [ 1, 5 ], 'y' ); | ||
output( '<p>123</p><p>4567xy[]8</p>' ); | ||
split( [ 1, 2 ] ); | ||
output( '<p>123</p><p>45</p><p>[]67xy8</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>123[]45</p><p>67xy8</p>' ); | ||
merge( [ 1 ] ); | ||
output( '<p>12345[]67xy8</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345</p><p>67xy8[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>45[]</p><p>67xy8</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>4567xy8[]</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>123</p><p>4567[]8</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>12345678[]</p>' ); | ||
undoDisabled(); | ||
editor.execute( 'redo' ); | ||
output( '<p>123[]</p><p>45678</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123</p><p>4567xy8[]</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>123</p><p>45[]</p><p>67xy8</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>12345[]</p><p>67xy8</p>' ); | ||
editor.execute( 'redo' ); | ||
output( '<p>1234567xy8[]</p>' ); | ||
redoDisabled(); | ||
} ); | ||
} ); | ||
describe( 'other reported cases', () => { | ||
// ckeditor5-engine#t/1051 | ||
it( 'rename leaks to other elements on undo #1', () => { | ||
input( '<h1>[]Foo</h1><p>Bar</p>' ); | ||
doc.batch().rename( root.getChild( 0 ), 'p' ); | ||
output( '<p>[]Foo</p><p>Bar</p>' ); | ||
doc.batch().split( Position.createAt( root.getChild( 0 ), 1 ) ); | ||
output( '<p>[]F</p><p>oo</p><p>Bar</p>' ); | ||
doc.batch().merge( Position.createAt( root, 2 ) ); | ||
output( '<p>[]F</p><p>ooBar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>[]F</p><p>oo</p><p>Bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>[]Foo</p><p>Bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<h1>[]Foo</h1><p>Bar</p>' ); | ||
} ); | ||
// Similar issue that bases on the same error as above, however here we first merge (above we first split). | ||
it( 'rename leaks to other elements on undo #2', () => { | ||
input( '<h1>[]Foo</h1><p>Bar</p>' ); | ||
doc.batch().rename( root.getChild( 0 ), 'h2' ); | ||
output( '<h2>[]Foo</h2><p>Bar</p>' ); | ||
doc.batch().merge( Position.createAt( root, 1 ) ); | ||
output( '<h2>[]FooBar</h2>' ); | ||
editor.execute( 'undo' ); | ||
output( '<h2>[]Foo</h2><p>Bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<h1>[]Foo</h1><p>Bar</p>' ); | ||
} ); | ||
// Reverse issue, this time first operation is merge and then rename. | ||
it( 'merge, rename, undo, undo is correct', () => { | ||
input( '<h1>[]Foo</h1><p>Bar</p>' ); | ||
doc.batch().merge( Position.createAt( root, 1 ) ); | ||
output( '<h1>[]FooBar</h1>' ); | ||
doc.batch().rename( root.getChild( 0 ), 'h2' ); | ||
output( '<h2>[]FooBar</h2>' ); | ||
editor.execute( 'undo' ); | ||
output( '<h1>[]FooBar</h1>' ); | ||
editor.execute( 'undo' ); | ||
output( '<h1>[]Foo</h1><p>Bar</p>' ); | ||
} ); | ||
// ckeditor5-engine#t/1053 | ||
it( 'wrap, split, undo, undo is correct', () => { | ||
input( '<p>[]Foo</p><p>Bar</p>' ); | ||
doc.batch().wrap( Range.createIn( root ), 'div' ); | ||
output( '<div><p>[]Foo</p><p>Bar</p></div>' ); | ||
doc.batch().split( new Position( root, [ 0, 0, 1 ] ) ); | ||
output( '<div><p>[]F</p><p>oo</p><p>Bar</p></div>' ); | ||
editor.execute( 'undo' ); | ||
output( '<div><p>[]Foo</p><p>Bar</p></div>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>[]Foo</p><p>Bar</p>' ); | ||
} ); | ||
// ckeditor5-engine#t/1055 | ||
it( 'selection attribute setting: split, bold, merge, undo, undo, undo', () => { | ||
input( '<p>Foo[]</p><p>Bar</p>' ); | ||
editor.execute( 'enter' ); | ||
output( '<p>Foo</p><p>[]</p><p>Bar</p>' ); | ||
editor.execute( 'bold' ); | ||
output( '<p>Foo</p><p selection:bold="true"><$text bold="true">[]</$text></p><p>Bar</p>' ); | ||
editor.execute( 'forwardDelete' ); | ||
output( '<p>Foo</p><p>[]Bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>Foo</p><p selection:bold="true"><$text bold="true">[]</$text></p><p>Bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>Foo</p><p>[]</p><p>Bar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>Foo[]</p><p>Bar</p>' ); | ||
} ); | ||
} ); | ||
describe( 'other edge cases', () => { | ||
it( 'deleteContent between two nodes', () => { | ||
input( '<p>fo[o</p><p>b]ar</p>' ); | ||
editor.data.deleteContent( doc.selection, doc.batch() ); | ||
output( '<p>fo[]ar</p>' ); | ||
editor.execute( 'undo' ); | ||
output( '<p>fo[o</p><p>b]ar</p>' ); | ||
} ); | ||
// Related to ckeditor5-engine#891 and ckeditor5-list#51. | ||
it( 'change attribute of removed node then undo and redo', () => { | ||
const gy = doc.graveyard; | ||
const batch = doc.batch(); | ||
const p = new Element( 'p' ); | ||
root.appendChildren( p ); | ||
batch.remove( p ); | ||
batch.setAttribute( p, 'bold', true ); | ||
editor.execute( 'undo' ); | ||
editor.execute( 'redo' ); | ||
expect( p.root ).to.equal( gy ); | ||
expect( p.getAttribute( 'bold' ) ).to.be.true; | ||
} ); | ||
// Related to ckeditor5-engine#891. | ||
it( 'change attribute of removed node then undo and redo', () => { | ||
const gy = doc.graveyard; | ||
const batch = doc.batch(); | ||
const p1 = new Element( 'p' ); | ||
const p2 = new Element( 'p' ); | ||
const p3 = new Element( 'p' ); | ||
root.appendChildren( [ p1, p2 ] ); | ||
batch.remove( p1 ).remove( p2 ).insert( new Position( root, [ 0 ] ), p3 ); | ||
editor.execute( 'undo' ); | ||
editor.execute( 'redo' ); | ||
expect( p1.root ).to.equal( gy ); | ||
expect( p2.root ).to.equal( gy ); | ||
expect( p3.root ).to.equal( root ); | ||
} ); | ||
} ); | ||
} ); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
54
1814
16
86423
11
+ Added@ckeditor/ckeditor5-core@0.9.0(transitive)
+ Added@ckeditor/ckeditor5-engine@0.11.0(transitive)
+ Added@ckeditor/ckeditor5-theme-lark@0.9.0(transitive)
+ Added@ckeditor/ckeditor5-ui@0.10.0(transitive)
+ Added@ckeditor/ckeditor5-utils@0.10.0(transitive)
- Removed@ckeditor/ckeditor5-core@0.8.1(transitive)
- Removed@ckeditor/ckeditor5-engine@0.10.0(transitive)
- Removed@ckeditor/ckeditor5-theme-lark@0.8.0(transitive)
- Removed@ckeditor/ckeditor5-ui@0.9.0(transitive)
- Removed@ckeditor/ckeditor5-utils@0.9.1(transitive)