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.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;
151 var contents = editor.getSnapshot(),
152 selection = contents && editor.getSelection();
154 // In IE, we need to remove the expando attributes.
155 CKEDITOR.env.ie && contents && ( contents = contents.replace( /\s+data-cke-expando=".*?"/g, '' ) );
157 this.contents = contents;
158 this.bookmarks = selection && selection.createBookmarks2( true );
161 // Attributes that browser may changing them when setting via innerHTML.
162 var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;
166 equals : function( otherImage, contentOnly )
169 var thisContents = this.contents,
170 otherContents = otherImage.contents;
172 // For IE6/7 : Comparing only the protected attribute values but not the original ones.(#4522)
173 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) )
175 thisContents = thisContents.replace( protectedAttrs, '' );
176 otherContents = otherContents.replace( protectedAttrs, '' );
179 if ( thisContents != otherContents )
185 var bookmarksA = this.bookmarks,
186 bookmarksB = otherImage.bookmarks;
188 if ( bookmarksA || bookmarksB )
190 if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )
193 for ( var i = 0 ; i < bookmarksA.length ; i++ )
195 var bookmarkA = bookmarksA[ i ],
196 bookmarkB = bookmarksB[ i ];
199 bookmarkA.startOffset != bookmarkB.startOffset ||
200 bookmarkA.endOffset != bookmarkB.endOffset ||
201 !CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) ||
202 !CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) )
214 * @constructor Main logic for Redo/Undo feature.
216 function UndoManager( editor )
218 this.editor = editor;
220 // Reset the undo stack.
225 var editingKeyCodes = { /*Backspace*/ 8:1, /*Delete*/ 46:1 },
226 modifierKeyCodes = { /*Shift*/ 16:1, /*Ctrl*/ 17:1, /*Alt*/ 18:1 },
227 navigationKeyCodes = { 37:1, 38:1, 39:1, 40:1 }; // Arrows: L, T, R, B
229 UndoManager.prototype =
232 * Process undo system regard keystrikes.
233 * @param {CKEDITOR.dom.event} event
235 type : function( event )
237 var keystroke = event && event.data.getKey(),
238 isModifierKey = keystroke in modifierKeyCodes,
239 isEditingKey = keystroke in editingKeyCodes,
240 wasEditingKey = this.lastKeystroke in editingKeyCodes,
241 sameAsLastEditingKey = isEditingKey && keystroke == this.lastKeystroke,
242 // Keystrokes which navigation through contents.
243 isReset = keystroke in navigationKeyCodes,
244 wasReset = this.lastKeystroke in navigationKeyCodes,
246 // Keystrokes which just introduce new contents.
247 isContent = ( !isEditingKey && !isReset ),
249 // Create undo snap for every different modifier key.
250 modifierSnapshot = ( isEditingKey && !sameAsLastEditingKey ),
251 // Create undo snap on the following cases:
252 // 1. Just start to type .
253 // 2. Typing some content after a modifier.
254 // 3. Typing some content after make a visible selection.
255 startedTyping = !( isModifierKey || this.typing )
256 || ( isContent && ( wasEditingKey || wasReset ) );
258 if ( startedTyping || modifierSnapshot )
260 var beforeTypeImage = new Image( this.editor );
262 // Use setTimeout, so we give the necessary time to the
263 // browser to insert the character into the DOM.
264 CKEDITOR.tools.setTimeout( function()
266 var currentSnapshot = this.editor.getSnapshot();
268 // In IE, we need to remove the expando attributes.
269 if ( CKEDITOR.env.ie )
270 currentSnapshot = currentSnapshot.replace( /\s+data-cke-expando=".*?"/g, '' );
272 if ( beforeTypeImage.contents != currentSnapshot )
274 // It's safe to now indicate typing state.
277 // This's a special save, with specified snapshot
278 // and without auto 'fireChange'.
279 if ( !this.save( false, beforeTypeImage, false ) )
280 // Drop future snapshots.
281 this.snapshots.splice( this.index + 1, this.snapshots.length - this.index - 1 );
284 this.hasRedo = false;
287 this.modifiersCount = 1;
296 this.lastKeystroke = keystroke;
298 // Create undo snap after typed too much (over 25 times).
302 this.modifiersCount++;
304 if ( this.modifiersCount > 25 )
306 this.save( false, null, false );
307 this.modifiersCount = 1;
312 this.modifiersCount = 0;
315 if ( this.typesCount > 25 )
317 this.save( false, null, false );
324 reset : function() // Reset the undo stack.
327 * Remember last pressed key.
329 this.lastKeystroke = 0;
332 * Stack for all the undo and redo snapshots, they're always created/removed
338 * Current snapshot history index.
342 this.limit = this.editor.config.undoStackSize || 20;
344 this.currentImage = null;
346 this.hasUndo = false;
347 this.hasRedo = false;
353 * Reset all states about typing.
354 * @see UndoManager.type
356 resetType : function()
359 delete this.lastKeystroke;
361 this.modifiersCount = 0;
363 fireChange : function()
365 this.hasUndo = !!this.getNextImage( true );
366 this.hasRedo = !!this.getNextImage( false );
373 * Save a snapshot of document image for later retrieve.
375 save : function( onContentOnly, image, autoFireChange )
377 var snapshots = this.snapshots;
379 // Get a content image.
381 image = new Image( this.editor );
383 // Do nothing if it was not possible to retrieve an image.
384 if ( image.contents === false )
387 // Check if this is a duplicate. In such case, do nothing.
388 if ( this.currentImage && image.equals( this.currentImage, onContentOnly ) )
391 // Drop future snapshots.
392 snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );
394 // If we have reached the limit, remove the oldest one.
395 if ( snapshots.length == this.limit )
398 // Add the new image, updating the current index.
399 this.index = snapshots.push( image ) - 1;
401 this.currentImage = image;
403 if ( autoFireChange !== false )
408 restoreImage : function( image )
410 this.editor.loadSnapshot( image.contents );
412 if ( image.bookmarks )
413 this.editor.getSelection().selectBookmarks( image.bookmarks );
414 else if ( CKEDITOR.env.ie )
416 // IE BUG: If I don't set the selection to *somewhere* after setting
417 // document contents, then IE would create an empty paragraph at the bottom
418 // the next time the document is modified.
419 var $range = this.editor.document.getBody().$.createTextRange();
420 $range.collapse( true );
424 this.index = image.index;
426 // Update current image with the actual editor
427 // content, since actualy content may differ from
428 // the original snapshot due to dom change. (#4622)
433 // Get the closest available image.
434 getNextImage : function( isUndo )
436 var snapshots = this.snapshots,
437 currentImage = this.currentImage,
444 for ( i = this.index - 1 ; i >= 0 ; i-- )
446 image = snapshots[ i ];
447 if ( !currentImage.equals( image, true ) )
456 for ( i = this.index + 1 ; i < snapshots.length ; i++ )
458 image = snapshots[ i ];
459 if ( !currentImage.equals( image, true ) )
472 * Check the current redo state.
473 * @return {Boolean} Whether the document has previous state to
476 redoable : function()
478 return this.enabled && this.hasRedo;
482 * Check the current undo state.
483 * @return {Boolean} Whether the document has future state to restore.
485 undoable : function()
487 return this.enabled && this.hasUndo;
491 * Perform undo on current index.
495 if ( this.undoable() )
499 var image = this.getNextImage( true );
501 return this.restoreImage( image ), true;
508 * Perform redo on current index.
512 if ( this.redoable() )
514 // Try to save. If no changes have been made, the redo stack
515 // will not change, so it will still be redoable.
518 // If instead we had changes, we can't redo anymore.
519 if ( this.redoable() )
521 var image = this.getNextImage( false );
523 return this.restoreImage( image ), true;
531 * Update the last snapshot of the undo stack with the current editor content.
535 this.snapshots.splice( this.index, 1, ( this.currentImage = new Image( this.editor ) ) );
541 * The number of undo steps to be saved. The higher this setting value the more
542 * memory is used for it.
546 * config.undoStackSize = 50;
550 * Fired when the editor is about to save an undo snapshot. This event can be
551 * fired by plugins and customizations to make the editor saving undo snapshots.
552 * @name CKEDITOR.editor#saveSnapshot