2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
\r
3 For licensing, see LICENSE.html or http://ckeditor.com/license
\r
7 * @fileOverview Undo/Redo system for saving shapshot for document modification
\r
8 * and other recordable changes.
\r
13 CKEDITOR.plugins.add( 'undo',
\r
15 requires : [ 'selection', 'wysiwygarea' ],
\r
17 init : function( editor )
\r
19 var undoManager = new UndoManager( editor );
\r
21 var undoCommand = editor.addCommand( 'undo',
\r
25 if ( undoManager.undo() )
\r
27 editor.selectionChange();
\r
28 this.fire( 'afterUndo' );
\r
31 state : CKEDITOR.TRISTATE_DISABLED,
\r
35 var redoCommand = editor.addCommand( 'redo',
\r
39 if ( undoManager.redo() )
\r
41 editor.selectionChange();
\r
42 this.fire( 'afterRedo' );
\r
45 state : CKEDITOR.TRISTATE_DISABLED,
\r
49 undoManager.onChange = function()
\r
51 undoCommand.setState( undoManager.undoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
\r
52 redoCommand.setState( undoManager.redoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
\r
55 function recordCommand( event )
\r
57 // If the command hasn't been marked to not support undo.
\r
58 if ( undoManager.enabled && event.data.command.canUndo !== false )
\r
62 // We'll save snapshots before and after executing a command.
\r
63 editor.on( 'beforeCommandExec', recordCommand );
\r
64 editor.on( 'afterCommandExec', recordCommand );
\r
66 // Save snapshots before doing custom changes.
\r
67 editor.on( 'saveSnapshot', function( evt )
\r
69 undoManager.save( evt.data && evt.data.contentOnly );
\r
72 // Registering keydown on every document recreation.(#3844)
\r
73 editor.on( 'contentDom', function()
\r
75 editor.document.on( 'keydown', function( event )
\r
77 // Do not capture CTRL hotkeys.
\r
78 if ( !event.data.$.ctrlKey && !event.data.$.metaKey )
\r
79 undoManager.type( event );
\r
83 // Always save an undo snapshot - the previous mode might have
\r
84 // changed editor contents.
\r
85 editor.on( 'beforeModeUnload', function()
\r
87 editor.mode == 'wysiwyg' && undoManager.save( true );
\r
90 // Make the undo manager available only in wysiwyg mode.
\r
91 editor.on( 'mode', function()
\r
93 undoManager.enabled = editor.readOnly ? false : editor.mode == 'wysiwyg';
\r
94 undoManager.onChange();
\r
97 editor.ui.addButton( 'Undo',
\r
99 label : editor.lang.undo,
\r
103 editor.ui.addButton( 'Redo',
\r
105 label : editor.lang.redo,
\r
109 editor.resetUndo = function()
\r
111 // Reset the undo stack.
\r
112 undoManager.reset();
\r
114 // Create the first image.
\r
115 editor.fire( 'saveSnapshot' );
\r
119 * Amend the top of undo stack (last undo image) with the current DOM changes.
\r
120 * @name CKEDITOR.editor#updateUndo
\r
124 * editor.fire( 'saveSnapshot' );
\r
125 * editor.document.body.append(...);
\r
126 * // Make new changes following the last undo snapshot part of it.
\r
127 * editor.fire( 'updateSnapshot' );
\r
131 editor.on( 'updateSnapshot', function()
\r
133 if ( undoManager.currentImage )
\r
134 undoManager.update();
\r
139 CKEDITOR.plugins.undo = {};
\r
142 * Undo snapshot which represents the current document status.
\r
143 * @name CKEDITOR.plugins.undo.Image
\r
144 * @param editor The editor instance on which the image is created.
\r
146 var Image = CKEDITOR.plugins.undo.Image = function( editor )
\r
148 this.editor = editor;
\r
150 editor.fire( 'beforeUndoImage' );
\r
152 var contents = editor.getSnapshot(),
\r
153 selection = contents && editor.getSelection();
\r
155 // In IE, we need to remove the expando attributes.
\r
156 CKEDITOR.env.ie && contents && ( contents = contents.replace( /\s+data-cke-expando=".*?"/g, '' ) );
\r
158 this.contents = contents;
\r
159 this.bookmarks = selection && selection.createBookmarks2( true );
\r
161 editor.fire( 'afterUndoImage' );
\r
164 // Attributes that browser may changing them when setting via innerHTML.
\r
165 var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;
\r
169 equals : function( otherImage, contentOnly )
\r
172 var thisContents = this.contents,
\r
173 otherContents = otherImage.contents;
\r
175 // For IE6/7 : Comparing only the protected attribute values but not the original ones.(#4522)
\r
176 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) )
\r
178 thisContents = thisContents.replace( protectedAttrs, '' );
\r
179 otherContents = otherContents.replace( protectedAttrs, '' );
\r
182 if ( thisContents != otherContents )
\r
188 var bookmarksA = this.bookmarks,
\r
189 bookmarksB = otherImage.bookmarks;
\r
191 if ( bookmarksA || bookmarksB )
\r
193 if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )
\r
196 for ( var i = 0 ; i < bookmarksA.length ; i++ )
\r
198 var bookmarkA = bookmarksA[ i ],
\r
199 bookmarkB = bookmarksB[ i ];
\r
202 bookmarkA.startOffset != bookmarkB.startOffset ||
\r
203 bookmarkA.endOffset != bookmarkB.endOffset ||
\r
204 !CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) ||
\r
205 !CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) )
\r
217 * @constructor Main logic for Redo/Undo feature.
\r
219 function UndoManager( editor )
\r
221 this.editor = editor;
\r
223 // Reset the undo stack.
\r
228 var editingKeyCodes = { /*Backspace*/ 8:1, /*Delete*/ 46:1 },
\r
229 modifierKeyCodes = { /*Shift*/ 16:1, /*Ctrl*/ 17:1, /*Alt*/ 18:1 },
\r
230 navigationKeyCodes = { 37:1, 38:1, 39:1, 40:1 }; // Arrows: L, T, R, B
\r
232 UndoManager.prototype =
\r
235 * Process undo system regard keystrikes.
\r
236 * @param {CKEDITOR.dom.event} event
\r
238 type : function( event )
\r
240 var keystroke = event && event.data.getKey(),
\r
241 isModifierKey = keystroke in modifierKeyCodes,
\r
242 isEditingKey = keystroke in editingKeyCodes,
\r
243 wasEditingKey = this.lastKeystroke in editingKeyCodes,
\r
244 sameAsLastEditingKey = isEditingKey && keystroke == this.lastKeystroke,
\r
245 // Keystrokes which navigation through contents.
\r
246 isReset = keystroke in navigationKeyCodes,
\r
247 wasReset = this.lastKeystroke in navigationKeyCodes,
\r
249 // Keystrokes which just introduce new contents.
\r
250 isContent = ( !isEditingKey && !isReset ),
\r
252 // Create undo snap for every different modifier key.
\r
253 modifierSnapshot = ( isEditingKey && !sameAsLastEditingKey ),
\r
254 // Create undo snap on the following cases:
\r
255 // 1. Just start to type .
\r
256 // 2. Typing some content after a modifier.
\r
257 // 3. Typing some content after make a visible selection.
\r
258 startedTyping = !( isModifierKey || this.typing )
\r
259 || ( isContent && ( wasEditingKey || wasReset ) );
\r
261 if ( startedTyping || modifierSnapshot )
\r
263 var beforeTypeImage = new Image( this.editor ),
\r
264 beforeTypeCount = this.snapshots.length;
\r
266 // Use setTimeout, so we give the necessary time to the
\r
267 // browser to insert the character into the DOM.
\r
268 CKEDITOR.tools.setTimeout( function()
\r
270 var currentSnapshot = this.editor.getSnapshot();
\r
272 // In IE, we need to remove the expando attributes.
\r
273 if ( CKEDITOR.env.ie )
\r
274 currentSnapshot = currentSnapshot.replace( /\s+data-cke-expando=".*?"/g, '' );
\r
276 // If changes have taken place, while not been captured yet (#8459),
\r
277 // compensate the snapshot.
\r
278 if ( beforeTypeImage.contents != currentSnapshot &&
\r
279 beforeTypeCount == this.snapshots.length )
\r
281 // It's safe to now indicate typing state.
\r
282 this.typing = true;
\r
284 // This's a special save, with specified snapshot
\r
285 // and without auto 'fireChange'.
\r
286 if ( !this.save( false, beforeTypeImage, false ) )
\r
287 // Drop future snapshots.
\r
288 this.snapshots.splice( this.index + 1, this.snapshots.length - this.index - 1 );
\r
290 this.hasUndo = true;
\r
291 this.hasRedo = false;
\r
293 this.typesCount = 1;
\r
294 this.modifiersCount = 1;
\r
303 this.lastKeystroke = keystroke;
\r
305 // Create undo snap after typed too much (over 25 times).
\r
306 if ( isEditingKey )
\r
308 this.typesCount = 0;
\r
309 this.modifiersCount++;
\r
311 if ( this.modifiersCount > 25 )
\r
313 this.save( false, null, false );
\r
314 this.modifiersCount = 1;
\r
317 else if ( !isReset )
\r
319 this.modifiersCount = 0;
\r
322 if ( this.typesCount > 25 )
\r
324 this.save( false, null, false );
\r
325 this.typesCount = 1;
\r
331 reset : function() // Reset the undo stack.
\r
334 * Remember last pressed key.
\r
336 this.lastKeystroke = 0;
\r
339 * Stack for all the undo and redo snapshots, they're always created/removed
\r
342 this.snapshots = [];
\r
345 * Current snapshot history index.
\r
349 this.limit = this.editor.config.undoStackSize || 20;
\r
351 this.currentImage = null;
\r
353 this.hasUndo = false;
\r
354 this.hasRedo = false;
\r
360 * Reset all states about typing.
\r
361 * @see UndoManager.type
\r
363 resetType : function()
\r
365 this.typing = false;
\r
366 delete this.lastKeystroke;
\r
367 this.typesCount = 0;
\r
368 this.modifiersCount = 0;
\r
370 fireChange : function()
\r
372 this.hasUndo = !!this.getNextImage( true );
\r
373 this.hasRedo = !!this.getNextImage( false );
\r
380 * Save a snapshot of document image for later retrieve.
\r
382 save : function( onContentOnly, image, autoFireChange )
\r
384 var snapshots = this.snapshots;
\r
386 // Get a content image.
\r
388 image = new Image( this.editor );
\r
390 // Do nothing if it was not possible to retrieve an image.
\r
391 if ( image.contents === false )
\r
394 // Check if this is a duplicate. In such case, do nothing.
\r
395 if ( this.currentImage && image.equals( this.currentImage, onContentOnly ) )
\r
398 // Drop future snapshots.
\r
399 snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );
\r
401 // If we have reached the limit, remove the oldest one.
\r
402 if ( snapshots.length == this.limit )
\r
405 // Add the new image, updating the current index.
\r
406 this.index = snapshots.push( image ) - 1;
\r
408 this.currentImage = image;
\r
410 if ( autoFireChange !== false )
\r
415 restoreImage : function( image )
\r
417 // Bring editor focused to restore selection.
\r
418 var editor = this.editor,
\r
421 if ( image.bookmarks )
\r
424 // Retrieve the selection beforehand. (#8324)
\r
425 sel = editor.getSelection();
\r
428 this.editor.loadSnapshot( image.contents );
\r
430 if ( image.bookmarks )
\r
431 sel.selectBookmarks( image.bookmarks );
\r
432 else if ( CKEDITOR.env.ie )
\r
434 // IE BUG: If I don't set the selection to *somewhere* after setting
\r
435 // document contents, then IE would create an empty paragraph at the bottom
\r
436 // the next time the document is modified.
\r
437 var $range = this.editor.document.getBody().$.createTextRange();
\r
438 $range.collapse( true );
\r
442 this.index = image.index;
\r
444 // Update current image with the actual editor
\r
445 // content, since actualy content may differ from
\r
446 // the original snapshot due to dom change. (#4622)
\r
451 // Get the closest available image.
\r
452 getNextImage : function( isUndo )
\r
454 var snapshots = this.snapshots,
\r
455 currentImage = this.currentImage,
\r
458 if ( currentImage )
\r
462 for ( i = this.index - 1 ; i >= 0 ; i-- )
\r
464 image = snapshots[ i ];
\r
465 if ( !currentImage.equals( image, true ) )
\r
474 for ( i = this.index + 1 ; i < snapshots.length ; i++ )
\r
476 image = snapshots[ i ];
\r
477 if ( !currentImage.equals( image, true ) )
\r
490 * Check the current redo state.
\r
491 * @return {Boolean} Whether the document has previous state to
\r
494 redoable : function()
\r
496 return this.enabled && this.hasRedo;
\r
500 * Check the current undo state.
\r
501 * @return {Boolean} Whether the document has future state to restore.
\r
503 undoable : function()
\r
505 return this.enabled && this.hasUndo;
\r
509 * Perform undo on current index.
\r
513 if ( this.undoable() )
\r
517 var image = this.getNextImage( true );
\r
519 return this.restoreImage( image ), true;
\r
526 * Perform redo on current index.
\r
530 if ( this.redoable() )
\r
532 // Try to save. If no changes have been made, the redo stack
\r
533 // will not change, so it will still be redoable.
\r
536 // If instead we had changes, we can't redo anymore.
\r
537 if ( this.redoable() )
\r
539 var image = this.getNextImage( false );
\r
541 return this.restoreImage( image ), true;
\r
549 * Update the last snapshot of the undo stack with the current editor content.
\r
551 update : function()
\r
553 this.snapshots.splice( this.index, 1, ( this.currentImage = new Image( this.editor ) ) );
\r
559 * The number of undo steps to be saved. The higher this setting value the more
\r
560 * memory is used for it.
\r
561 * @name CKEDITOR.config.undoStackSize
\r
565 * config.undoStackSize = 50;
\r
569 * Fired when the editor is about to save an undo snapshot. This event can be
\r
570 * fired by plugins and customizations to make the editor saving undo snapshots.
\r
571 * @name CKEDITOR.editor#saveSnapshot
\r
576 * Fired before an undo image is to be taken. An undo image represents the
\r
577 * editor state at some point. It's saved into an undo store, so the editor is
\r
578 * able to recover the editor state on undo and redo operations.
\r
579 * @name CKEDITOR.editor#beforeUndoImage
\r
581 * @see CKEDITOR.editor#afterUndoImage
\r
586 * Fired after an undo image is taken. An undo image represents the
\r
587 * editor state at some point. It's saved into an undo store, so the editor is
\r
588 * able to recover the editor state on undo and redo operations.
\r
589 * @name CKEDITOR.editor#afterUndoImage
\r
591 * @see CKEDITOR.editor#beforeUndoImage
\r