This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
+1,008
−0
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
3c348a6
Undo Feature initial commit.
scofalik 4420f98
Changes according to changes in CKE5-core.
scofalik 9eff507
Added: undo.UndoCommand selection restoring + docs update.
scofalik f89097a
Tests, docs: undo.UndoFeature minor tweaks.
scofalik 322c80e
Fixes: UndoFeature/UndoCommand changes Map/Set to WeakMap/WeakSet.
scofalik 2eda14d
Changed: UndoCommand now restores selection direction. + fixes after …
scofalik 82645d6
Changed: Multiple changes in Undo after review.
scofalik 36484d6
Changed: removing commented code + docs.
scofalik 3a5542e
Changed: Fixes after review.
scofalik 89885e9
Tests: Commented out tests that fail.
scofalik bdf3ba9
Renamed _batchStack to more correct _items and made minor simplificat…
Reinmar 859f639
Minor API docs corrections.
Reinmar fdc695a
Minor improvement.
Reinmar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
/** | ||
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
import Feature from '../feature.js'; | ||
import UndoCommand from './undocommand.js'; | ||
|
||
/** | ||
* Undo feature. | ||
* | ||
* Undo features brings in possibility to undo and re-do changes done in Tree Model by deltas through Batch API. | ||
* | ||
* @memberOf undo | ||
*/ | ||
export default class Undo extends Feature { | ||
constructor( editor ) { | ||
super( editor ); | ||
|
||
/** | ||
* Undo command which manages undo {@link engine.treeModel.Batch batches} stack (history). | ||
* Created and registered during {@link undo.Undo#init feature initialization}. | ||
* | ||
* @private | ||
* @member {undo.UndoCommand} undo.Undo#_undoCommand | ||
*/ | ||
this._undoCommand = null; | ||
|
||
/** | ||
* Undo command which manages redo {@link engine.treeModel.Batch batches} stack (history). | ||
* Created and registered during {@link undo.Undo#init feature initialization}. | ||
* | ||
* @private | ||
* @member {undo.UndoCommand} undo.Undo#_redoCommand | ||
*/ | ||
this._redoCommand = null; | ||
|
||
/** | ||
* Keeps track of which batch has already been added to undo manager. | ||
* | ||
* @private | ||
* @member {WeakSet.<engine.treeModel.Batch>} undo.Undo#_batchRegistry | ||
*/ | ||
this._batchRegistry = new WeakSet(); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
init() { | ||
// Create commands. | ||
this._redoCommand = new UndoCommand( this.editor ); | ||
this._undoCommand = new UndoCommand( this.editor ); | ||
|
||
// Register command to the editor. | ||
this.editor.commands.set( 'redo', this._redoCommand ); | ||
this.editor.commands.set( 'undo', this._undoCommand ); | ||
|
||
this.listenTo( this.editor.document, 'change', ( evt, type, changes, batch ) => { | ||
// Whenever a new batch is created add it to the undo history and clear redo history. | ||
if ( batch && !this._batchRegistry.has( batch ) ) { | ||
this._batchRegistry.add( batch ); | ||
this._undoCommand.addBatch( batch ); | ||
this._redoCommand.clearStack(); | ||
} | ||
} ); | ||
|
||
// Whenever batch is reverted by undo command, add it to redo history. | ||
this.listenTo( this._redoCommand, 'revert', ( evt, batch ) => { | ||
this._undoCommand.addBatch( batch ); | ||
} ); | ||
|
||
// Whenever batch is reverted by redo command, add it to undo history. | ||
this.listenTo( this._undoCommand, 'revert', ( evt, batch ) => { | ||
this._redoCommand.addBatch( batch ); | ||
} ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
/** | ||
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md. | ||
*/ | ||
|
||
'use strict'; | ||
|
||
import Command from '../command/command.js'; | ||
|
||
/** | ||
* Undo command stores batches in itself and is able to and apply reverted versions of them on the document. | ||
* | ||
* @memberOf undo | ||
*/ | ||
export default class UndoCommand extends Command { | ||
constructor( editor ) { | ||
super( editor ); | ||
|
||
/** | ||
* Items that are pairs of: | ||
* | ||
* * batches which are saved by the command and, | ||
* * model selection state at the moment of saving the batch. | ||
* | ||
* @private | ||
* @member {Array} undo.UndoCommand#_items | ||
*/ | ||
this._items = []; | ||
} | ||
|
||
/** | ||
* Stores a batch in the command. Stored batches can be then reverted. | ||
* | ||
* @param {engine.treeModel.Batch} batch Batch to add. | ||
*/ | ||
addBatch( batch ) { | ||
const selection = { | ||
ranges: Array.from( this.editor.document.selection.getRanges() ), | ||
isBackward: this.editor.document.selection.isBackward | ||
}; | ||
|
||
this._items.push( { batch, selection } ); | ||
this.refreshState(); | ||
} | ||
|
||
/** | ||
* Removes all batches from the stack. | ||
*/ | ||
clearStack() { | ||
this._items = []; | ||
this.refreshState(); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
_checkEnabled() { | ||
return this._items.length > 0; | ||
} | ||
|
||
/** | ||
* Executes the command: reverts a {@link engine.treeModel.Batch batch} added to the command's stack, | ||
* applies it on the document and removes the batch from the stack. | ||
* | ||
* @protected | ||
* @fires undo.undoCommand#event:revert | ||
* @param {engine.treeModel.Batch} [batch] If set, batch that should be undone. If not set, the last added batch will be undone. | ||
*/ | ||
_doExecute( batch ) { | ||
let batchIndex; | ||
|
||
// If batch is not given, set `batchIndex` to the last index in command stack. | ||
// If it is given, find it on the stack. | ||
if ( !batch ) { | ||
batchIndex = this._items.length - 1; | ||
} else { | ||
batchIndex = this._items.findIndex( item => item.batch == batch ); | ||
} | ||
|
||
const undoItem = this._items.splice( batchIndex, 1 )[ 0 ]; | ||
|
||
// Get the batch to undo. | ||
const undoBatch = undoItem.batch; | ||
const undoDeltas = undoBatch.deltas.slice(); | ||
// Deltas have to be applied in reverse order, so if batch did A B C, it has to do reversed C, reversed B, reversed A. | ||
undoDeltas.reverse(); | ||
|
||
// Reverse the deltas from the batch, transform them, apply them. | ||
for ( let undoDelta of undoDeltas ) { | ||
const undoDeltaReversed = undoDelta.getReversed(); | ||
const updatedDeltas = this.editor.document.history.getTransformedDelta( undoDeltaReversed ); | ||
|
||
for ( let delta of updatedDeltas ) { | ||
for ( let operation of delta.operations ) { | ||
this.editor.document.applyOperation( operation ); | ||
} | ||
} | ||
} | ||
|
||
// Get the selection state stored with this batch. | ||
const selectionState = undoItem.selection; | ||
|
||
// Take all selection ranges that were stored with undone batch. | ||
const ranges = selectionState.ranges; | ||
|
||
// The ranges will be transformed by deltas from history that took place | ||
// after the selection got stored. | ||
const deltas = this.editor.document.history.getDeltas( undoBatch.deltas[ 0 ].baseVersion ); | ||
|
||
// This will keep the transformed ranges. | ||
const transformedRanges = []; | ||
|
||
for ( let originalRange of ranges ) { | ||
// We create `transformed` array. At the beginning it will have only the original range. | ||
// During transformation the original range will change or even break into smaller ranges. | ||
// After the range is broken into two ranges, we have to transform both of those ranges separately. | ||
// For that reason, we keep all transformed ranges in one array and operate on it. | ||
let transformed = [ originalRange ]; | ||
|
||
for ( let delta of deltas ) { | ||
for ( let operation of delta.operations ) { | ||
// We look through all operations from all deltas. | ||
|
||
for ( let t = 0; t < transformed.length; t++ ) { | ||
// We transform every range by every operation. | ||
// We keep current state of transformation in `transformed` array and update it. | ||
let result; | ||
|
||
switch ( operation.type ) { | ||
case 'insert': | ||
result = transformed[ t ].getTransformedByInsertion( | ||
operation.position, | ||
operation.nodeList.length, | ||
true | ||
); | ||
break; | ||
|
||
case 'move': | ||
case 'remove': | ||
case 'reinsert': | ||
result = transformed[ t ].getTransformedByMove( | ||
operation.sourcePosition, | ||
operation.targetPosition, | ||
operation.howMany, | ||
true | ||
); | ||
break; | ||
} | ||
|
||
// If we have a transformation result, we substitute it in `transformed` array with | ||
// the range that got transformed. Keep in mind that the result is an array | ||
// and may contain multiple ranges. | ||
if ( result ) { | ||
transformed.splice( t, 1, ...result ); | ||
|
||
// Fix iterator. | ||
t = t + result.length - 1; | ||
} | ||
} | ||
} | ||
} | ||
|
||
// After `originalRange` 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. | ||
// Let's do this. First, we have to sort those ranges because they don't have to be | ||
// in an order. | ||
transformed.sort( ( a, b ) => a.start.isBefore( b.start ) ? -1 : 1 ); | ||
|
||
// Then we check if two consecutive ranges are touching. We can do it pair by pair | ||
// in one dimensional loop because ranges are sorted. | ||
for ( let i = 1 ; i < transformed.length; i++ ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just wanted to leave some comment in the middle of this function, so it looks like we've read and understood that code. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's fairly simple, I can explain ... |
||
let a = transformed[ i - 1 ]; | ||
let b = transformed[ i ]; | ||
|
||
if ( a.end.isTouching( b.start ) ) { | ||
a.end = b.end; | ||
transformed.splice( i, 1 ); | ||
i--; | ||
} | ||
} | ||
|
||
// For each `originalRange` from `ranges`, we take only one transformed range. | ||
// This is because we want to prevent situation where single-range selection | ||
// got transformed to mulit-range selection. We will take the first range that | ||
// is not in the graveyard. | ||
const transformedRange = transformed.find( | ||
( range ) => range.start.root != this.editor.document.graveyard | ||
); | ||
|
||
if ( transformedRange ) { | ||
transformedRanges.push( transformedRange ); | ||
} | ||
} | ||
|
||
// `transformedRanges` may be empty if all ranges ended up in graveyard. | ||
// If that is the case, do not restore selection. | ||
if ( transformedRanges.length ) { | ||
this.editor.document.selection.setRanges( transformedRanges, selectionState.isBackward ); | ||
} | ||
|
||
this.refreshState(); | ||
this.fire( 'revert', undoBatch ); | ||
} | ||
} | ||
|
||
/** | ||
* Fired after `UndoCommand` reverts a batch. | ||
* | ||
* @event undo.UndoCommand#revert | ||
* @param {engine.treeModel.Batch} undoBatch The batch instance that got reverted. | ||
*/ |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Misses docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, it's inherited from
Command
. Hmm... so maybe, to avoid confusion, we could use@inheritDoc
in such cases. Then it will be clear that the doc comes from the parent.