2 Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.
3 For licensing, see LICENSE.html or http://ckeditor.com/license
7 * @fileOverview Undo/Redo system for saving shapshot for document modification
8 * and other recordable changes.
13 CKEDITOR
.plugins
.add( 'undo',
15 requires
: [ 'selection', 'wysiwygarea' ],
17 init : function( editor
)
19 var undoManager
= new UndoManager( editor
);
21 var undoCommand
= editor
.addCommand( 'undo',
25 if ( undoManager
.undo() )
27 editor
.selectionChange();
28 this.fire( 'afterUndo' );
31 state
: CKEDITOR
.TRISTATE_DISABLED
,
35 var redoCommand
= editor
.addCommand( 'redo',
39 if ( undoManager
.redo() )
41 editor
.selectionChange();
42 this.fire( 'afterRedo' );
45 state
: CKEDITOR
.TRISTATE_DISABLED
,
49 undoManager
.onChange = function()
51 undoCommand
.setState( undoManager
.undoable() ? CKEDITOR
.TRISTATE_OFF
: CKEDITOR
.TRISTATE_DISABLED
);
52 redoCommand
.setState( undoManager
.redoable() ? CKEDITOR
.TRISTATE_OFF
: CKEDITOR
.TRISTATE_DISABLED
);
55 function recordCommand( event
)
57 // If the command hasn't been marked to not support undo.
58 if ( undoManager
.enabled
&& event
.data
.command
.canUndo
!== false )
62 // We'll save snapshots before and after executing a command.
63 editor
.on( 'beforeCommandExec', recordCommand
);
64 editor
.on( 'afterCommandExec', recordCommand
);
66 // Save snapshots before doing custom changes.
67 editor
.on( 'saveSnapshot', function()
72 // Registering keydown on every document recreation.(#3844)
73 editor
.on( 'contentDom', function()
75 editor
.document
.on( 'keydown', function( event
)
77 // Do not capture CTRL hotkeys.
78 if ( !event
.data
.$.ctrlKey
&& !event
.data
.$.metaKey
)
79 undoManager
.type( event
);
83 // Always save an undo snapshot - the previous mode might have
84 // changed editor contents.
85 editor
.on( 'beforeModeUnload', function()
87 editor
.mode
== 'wysiwyg' && undoManager
.save( true );
90 // Make the undo manager available only in wysiwyg mode.
91 editor
.on( 'mode', function()
93 undoManager
.enabled
= editor
.readOnly
? false : editor
.mode
== 'wysiwyg';
94 undoManager
.onChange();
97 editor
.ui
.addButton( 'Undo',
99 label
: editor
.lang
.undo
,
103 editor
.ui
.addButton( 'Redo',
105 label
: editor
.lang
.redo
,
109 editor
.resetUndo = function()
111 // Reset the undo stack.
114 // Create the first image.
115 editor
.fire( 'saveSnapshot' );
119 * Update the undo stacks with any subsequent DOM changes after this call.
120 * @name CKEDITOR.editor#updateUndo
124 * editor.fire( 'updateSnapshot' );
126 * // Ask to include subsequent (in this call stack) DOM changes to be
127 * // considered as part of the first snapshot.
128 * editor.fire( 'updateSnapshot' );
129 * editor.document.body.append(...);
133 editor
.on( 'updateSnapshot', function()
135 if ( undoManager
.currentImage
&& new Image( editor
).equals( undoManager
.currentImage
) )
136 setTimeout( function() { undoManager
.update(); }, 0 );
141 CKEDITOR
.plugins
.undo
= {};
144 * Undo snapshot which represents the current document status.
145 * @name CKEDITOR.plugins.undo.Image
146 * @param editor The editor instance on which the image is created.
148 var Image
= CKEDITOR
.plugins
.undo
.Image = function( editor
)
150 this.editor
= editor
;
152 editor
.fire( 'beforeUndoImage' );
154 var contents
= editor
.getSnapshot(),
155 selection
= contents
&& editor
.getSelection();
157 // In IE, we need to remove the expando attributes.
158 CKEDITOR
.env
.ie
&& contents
&& ( contents
= contents
.replace( /\s+data-cke-expando=".*?"/g, '' ) );
160 this.contents
= contents
;
161 this.bookmarks
= selection
&& selection
.createBookmarks2( true );
163 editor
.fire( 'afterUndoImage' );
166 // Attributes that browser may changing them when setting via innerHTML.
167 var protectedAttrs
= /\b(?:href|src|name)="[^"]*?"/gi;
171 equals : function( otherImage
, contentOnly
)
174 var thisContents
= this.contents
,
175 otherContents
= otherImage
.contents
;
177 // For IE6/7 : Comparing only the protected attribute values but not the original ones.(#4522)
178 if ( CKEDITOR
.env
.ie
&& ( CKEDITOR
.env
.ie7Compat
|| CKEDITOR
.env
.ie6Compat
) )
180 thisContents
= thisContents
.replace( protectedAttrs
, '' );
181 otherContents
= otherContents
.replace( protectedAttrs
, '' );
184 if ( thisContents
!= otherContents
)
190 var bookmarksA
= this.bookmarks
,
191 bookmarksB
= otherImage
.bookmarks
;
193 if ( bookmarksA
|| bookmarksB
)
195 if ( !bookmarksA
|| !bookmarksB
|| bookmarksA
.length
!= bookmarksB
.length
)
198 for ( var i
= 0 ; i
< bookmarksA
.length
; i
++ )
200 var bookmarkA
= bookmarksA
[ i
],
201 bookmarkB
= bookmarksB
[ i
];
204 bookmarkA
.startOffset
!= bookmarkB
.startOffset
||
205 bookmarkA
.endOffset
!= bookmarkB
.endOffset
||
206 !CKEDITOR
.tools
.arrayCompare( bookmarkA
.start
, bookmarkB
.start
) ||
207 !CKEDITOR
.tools
.arrayCompare( bookmarkA
.end
, bookmarkB
.end
) )
219 * @constructor Main logic for Redo/Undo feature.
221 function UndoManager( editor
)
223 this.editor
= editor
;
225 // Reset the undo stack.
230 var editingKeyCodes
= { /*Backspace*/ 8:1, /*Delete*/ 46:1 },
231 modifierKeyCodes
= { /*Shift*/ 16:1, /*Ctrl*/ 17:1, /*Alt*/ 18:1 },
232 navigationKeyCodes
= { 37:1, 38:1, 39:1, 40:1 }; // Arrows: L, T, R, B
234 UndoManager
.prototype =
237 * Process undo system regard keystrikes.
238 * @param {CKEDITOR.dom.event} event
240 type : function( event
)
242 var keystroke
= event
&& event
.data
.getKey(),
243 isModifierKey
= keystroke
in modifierKeyCodes
,
244 isEditingKey
= keystroke
in editingKeyCodes
,
245 wasEditingKey
= this.lastKeystroke
in editingKeyCodes
,
246 sameAsLastEditingKey
= isEditingKey
&& keystroke
== this.lastKeystroke
,
247 // Keystrokes which navigation through contents.
248 isReset
= keystroke
in navigationKeyCodes
,
249 wasReset
= this.lastKeystroke
in navigationKeyCodes
,
251 // Keystrokes which just introduce new contents.
252 isContent
= ( !isEditingKey
&& !isReset
),
254 // Create undo snap for every different modifier key.
255 modifierSnapshot
= ( isEditingKey
&& !sameAsLastEditingKey
),
256 // Create undo snap on the following cases:
257 // 1. Just start to type .
258 // 2. Typing some content after a modifier.
259 // 3. Typing some content after make a visible selection.
260 startedTyping
= !( isModifierKey
|| this.typing
)
261 || ( isContent
&& ( wasEditingKey
|| wasReset
) );
263 if ( startedTyping
|| modifierSnapshot
)
265 var beforeTypeImage
= new Image( this.editor
);
267 // Use setTimeout, so we give the necessary time to the
268 // browser to insert the character into the DOM.
269 CKEDITOR
.tools
.setTimeout( function()
271 var currentSnapshot
= this.editor
.getSnapshot();
273 // In IE, we need to remove the expando attributes.
274 if ( CKEDITOR
.env
.ie
)
275 currentSnapshot
= currentSnapshot
.replace( /\s+data-cke-expando=".*?"/g, '' );
277 if ( beforeTypeImage
.contents
!= currentSnapshot
)
279 // It's safe to now indicate typing state.
282 // This's a special save, with specified snapshot
283 // and without auto 'fireChange'.
284 if ( !this.save( false, beforeTypeImage
, false ) )
285 // Drop future snapshots.
286 this.snapshots
.splice( this.index
+ 1, this.snapshots
.length
- this.index
- 1 );
289 this.hasRedo
= false;
292 this.modifiersCount
= 1;
301 this.lastKeystroke
= keystroke
;
303 // Create undo snap after typed too much (over 25 times).
307 this.modifiersCount
++;
309 if ( this.modifiersCount
> 25 )
311 this.save( false, null, false );
312 this.modifiersCount
= 1;
317 this.modifiersCount
= 0;
320 if ( this.typesCount
> 25 )
322 this.save( false, null, false );
329 reset : function() // Reset the undo stack.
332 * Remember last pressed key.
334 this.lastKeystroke
= 0;
337 * Stack for all the undo and redo snapshots, they're always created/removed
343 * Current snapshot history index.
347 this.limit
= this.editor
.config
.undoStackSize
|| 20;
349 this.currentImage
= null;
351 this.hasUndo
= false;
352 this.hasRedo
= false;
358 * Reset all states about typing.
359 * @see UndoManager.type
361 resetType : function()
364 delete this.lastKeystroke
;
366 this.modifiersCount
= 0;
368 fireChange : function()
370 this.hasUndo
= !!this.getNextImage( true );
371 this.hasRedo
= !!this.getNextImage( false );
378 * Save a snapshot of document image for later retrieve.
380 save : function( onContentOnly
, image
, autoFireChange
)
382 var snapshots
= this.snapshots
;
384 // Get a content image.
386 image
= new Image( this.editor
);
388 // Do nothing if it was not possible to retrieve an image.
389 if ( image
.contents
=== false )
392 // Check if this is a duplicate. In such case, do nothing.
393 if ( this.currentImage
&& image
.equals( this.currentImage
, onContentOnly
) )
396 // Drop future snapshots.
397 snapshots
.splice( this.index
+ 1, snapshots
.length
- this.index
- 1 );
399 // If we have reached the limit, remove the oldest one.
400 if ( snapshots
.length
== this.limit
)
403 // Add the new image, updating the current index.
404 this.index
= snapshots
.push( image
) - 1;
406 this.currentImage
= image
;
408 if ( autoFireChange
!== false )
413 restoreImage : function( image
)
415 this.editor
.loadSnapshot( image
.contents
);
417 if ( image
.bookmarks
)
418 this.editor
.getSelection().selectBookmarks( image
.bookmarks
);
419 else if ( CKEDITOR
.env
.ie
)
421 // IE BUG: If I don't set the selection to *somewhere* after setting
422 // document contents, then IE would create an empty paragraph at the bottom
423 // the next time the document is modified.
424 var $range
= this.editor
.document
.getBody().$.createTextRange();
425 $range
.collapse( true );
429 this.index
= image
.index
;
431 // Update current image with the actual editor
432 // content, since actualy content may differ from
433 // the original snapshot due to dom change. (#4622)
438 // Get the closest available image.
439 getNextImage : function( isUndo
)
441 var snapshots
= this.snapshots
,
442 currentImage
= this.currentImage
,
449 for ( i
= this.index
- 1 ; i
>= 0 ; i
-- )
451 image
= snapshots
[ i
];
452 if ( !currentImage
.equals( image
, true ) )
461 for ( i
= this.index
+ 1 ; i
< snapshots
.length
; i
++ )
463 image
= snapshots
[ i
];
464 if ( !currentImage
.equals( image
, true ) )
477 * Check the current redo state.
478 * @return {Boolean} Whether the document has previous state to
481 redoable : function()
483 return this.enabled
&& this.hasRedo
;
487 * Check the current undo state.
488 * @return {Boolean} Whether the document has future state to restore.
490 undoable : function()
492 return this.enabled
&& this.hasUndo
;
496 * Perform undo on current index.
500 if ( this.undoable() )
504 var image
= this.getNextImage( true );
506 return this.restoreImage( image
), true;
513 * Perform redo on current index.
517 if ( this.redoable() )
519 // Try to save. If no changes have been made, the redo stack
520 // will not change, so it will still be redoable.
523 // If instead we had changes, we can't redo anymore.
524 if ( this.redoable() )
526 var image
= this.getNextImage( false );
528 return this.restoreImage( image
), true;
536 * Update the last snapshot of the undo stack with the current editor content.
540 this.snapshots
.splice( this.index
, 1, ( this.currentImage
= new Image( this.editor
) ) );
546 * The number of undo steps to be saved. The higher this setting value the more
547 * memory is used for it.
548 * @name CKEDITOR.config.undoStackSize
552 * config.undoStackSize = 50;
556 * Fired when the editor is about to save an undo snapshot. This event can be
557 * fired by plugins and customizations to make the editor saving undo snapshots.
558 * @name CKEDITOR.editor#saveSnapshot
563 * Fired before an undo image is to be taken. An undo image represents the
564 * editor state at some point. It's saved into an undo store, so the editor is
565 * able to recover the editor state on undo and redo operations.
566 * @name CKEDITOR.editor#beforeUndoImage
568 * @see CKEDITOR.editor#afterUndoImage
573 * Fired after an undo image is taken. An undo image represents the
574 * editor state at some point. It's saved into an undo store, so the editor is
575 * able to recover the editor state on undo and redo operations.
576 * @name CKEDITOR.editor#afterUndoImage
578 * @see CKEDITOR.editor#beforeUndoImage