1 /*
  2 Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
  3 For licensing, see LICENSE.html or http://ckeditor.com/license
  4 */
  5 
  6 /**
  7  * @fileOverview Undo/Redo system for saving shapshot for document modification
  8  *		and other recordable changes.
  9  */
 10 
 11 (function()
 12 {
 13 	CKEDITOR.plugins.add( 'undo',
 14 	{
 15 		requires : [ 'selection', 'wysiwygarea' ],
 16 
 17 		init : function( editor )
 18 		{
 19 			var undoManager = new UndoManager( editor );
 20 
 21 			var undoCommand = editor.addCommand( 'undo',
 22 				{
 23 					exec : function()
 24 					{
 25 						if ( undoManager.undo() )
 26 						{
 27 							editor.selectionChange();
 28 							this.fire( 'afterUndo' );
 29 						}
 30 					},
 31 					state : CKEDITOR.TRISTATE_DISABLED,
 32 					canUndo : false
 33 				});
 34 
 35 			var redoCommand = editor.addCommand( 'redo',
 36 				{
 37 					exec : function()
 38 					{
 39 						if ( undoManager.redo() )
 40 						{
 41 							editor.selectionChange();
 42 							this.fire( 'afterRedo' );
 43 						}
 44 					},
 45 					state : CKEDITOR.TRISTATE_DISABLED,
 46 					canUndo : false
 47 				});
 48 
 49 			undoManager.onChange = function()
 50 			{
 51 				undoCommand.setState( undoManager.undoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
 52 				redoCommand.setState( undoManager.redoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );
 53 			};
 54 
 55 			function recordCommand( event )
 56 			{
 57 				// If the command hasn't been marked to not support undo.
 58 				if ( undoManager.enabled && event.data.command.canUndo !== false )
 59 					undoManager.save();
 60 			}
 61 
 62 			// We'll save snapshots before and after executing a command.
 63 			editor.on( 'beforeCommandExec', recordCommand );
 64 			editor.on( 'afterCommandExec', recordCommand );
 65 
 66 			// Save snapshots before doing custom changes.
 67 			editor.on( 'saveSnapshot', function( evt )
 68 				{
 69 					undoManager.save( evt.data && evt.data.contentOnly );
 70 				});
 71 
 72 			// Registering keydown on every document recreation.(#3844)
 73 			editor.on( 'contentDom', function()
 74 				{
 75 					editor.document.on( 'keydown', function( event )
 76 						{
 77 							// Do not capture CTRL hotkeys.
 78 							if ( !event.data.$.ctrlKey && !event.data.$.metaKey )
 79 								undoManager.type( event );
 80 						});
 81 				});
 82 
 83 			// Always save an undo snapshot - the previous mode might have
 84 			// changed editor contents.
 85 			editor.on( 'beforeModeUnload', function()
 86 				{
 87 					editor.mode == 'wysiwyg' && undoManager.save( true );
 88 				});
 89 
 90 			// Make the undo manager available only in wysiwyg mode.
 91 			editor.on( 'mode', function()
 92 				{
 93 					undoManager.enabled = editor.readOnly ? false : editor.mode == 'wysiwyg';
 94 					undoManager.onChange();
 95 				});
 96 
 97 			editor.ui.addButton( 'Undo',
 98 				{
 99 					label : editor.lang.undo,
100 					command : 'undo'
101 				});
102 
103 			editor.ui.addButton( 'Redo',
104 				{
105 					label : editor.lang.redo,
106 					command : 'redo'
107 				});
108 
109 			editor.resetUndo = function()
110 			{
111 				// Reset the undo stack.
112 				undoManager.reset();
113 
114 				// Create the first image.
115 				editor.fire( 'saveSnapshot' );
116 			};
117 
118 			/**
119 			 * Amend the top of undo stack (last undo image) with the current DOM changes.
120 			 * @name CKEDITOR.editor#updateUndo
121 			 * @example
122 			 * function()
123 			 * {
124 			 *  editor.fire( 'saveSnapshot' );
125 			 * 	editor.document.body.append(...);
126 			 *  // Make new changes following the last undo snapshot part of it.
127 			 * 	editor.fire( 'updateSnapshot' );
128 			 * ...
129 			 * }
130 			 */
131 			editor.on( 'updateSnapshot', function()
132 			{
133 				if ( undoManager.currentImage )
134 					undoManager.update();
135 			});
136 		}
137 	});
138 
139 	CKEDITOR.plugins.undo = {};
140 
141 	/**
142 	 * Undo snapshot which represents the current document status.
143 	 * @name CKEDITOR.plugins.undo.Image
144 	 * @param editor The editor instance on which the image is created.
145 	 */
146 	var Image = CKEDITOR.plugins.undo.Image = function( editor )
147 	{
148 		this.editor = editor;
149 
150 		editor.fire( 'beforeUndoImage' );
151 
152 		var contents = editor.getSnapshot(),
153 			selection	= contents && editor.getSelection();
154 
155 		// In IE, we need to remove the expando attributes.
156 		CKEDITOR.env.ie && contents && ( contents = contents.replace( /\s+data-cke-expando=".*?"/g, '' ) );
157 
158 		this.contents	= contents;
159 		this.bookmarks	= selection && selection.createBookmarks2( true );
160 
161 		editor.fire( 'afterUndoImage' );
162 	};
163 
164 	// Attributes that browser may changing them when setting via innerHTML.
165 	var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;
166 
167 	Image.prototype =
168 	{
169 		equals : function( otherImage, contentOnly )
170 		{
171 
172 			var thisContents = this.contents,
173 				otherContents = otherImage.contents;
174 
175 			// For IE6/7 : Comparing only the protected attribute values but not the original ones.(#4522)
176 			if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) )
177 			{
178 				thisContents = thisContents.replace( protectedAttrs, '' );
179 				otherContents = otherContents.replace( protectedAttrs, '' );
180 			}
181 
182 			if ( thisContents != otherContents )
183 				return false;
184 
185 			if ( contentOnly )
186 				return true;
187 
188 			var bookmarksA = this.bookmarks,
189 				bookmarksB = otherImage.bookmarks;
190 
191 			if ( bookmarksA || bookmarksB )
192 			{
193 				if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )
194 					return false;
195 
196 				for ( var i = 0 ; i < bookmarksA.length ; i++ )
197 				{
198 					var bookmarkA = bookmarksA[ i ],
199 						bookmarkB = bookmarksB[ i ];
200 
201 					if (
202 						bookmarkA.startOffset != bookmarkB.startOffset ||
203 						bookmarkA.endOffset != bookmarkB.endOffset ||
204 						!CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) ||
205 						!CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) )
206 					{
207 						return false;
208 					}
209 				}
210 			}
211 
212 			return true;
213 		}
214 	};
215 
216 	/**
217 	 * @constructor Main logic for Redo/Undo feature.
218 	 */
219 	function UndoManager( editor )
220 	{
221 		this.editor = editor;
222 
223 		// Reset the undo stack.
224 		this.reset();
225 	}
226 
227 
228 	var editingKeyCodes = { /*Backspace*/ 8:1, /*Delete*/ 46:1 },
229 		modifierKeyCodes = { /*Shift*/ 16:1, /*Ctrl*/ 17:1, /*Alt*/ 18:1 },
230 		navigationKeyCodes = { 37:1, 38:1, 39:1, 40:1 };  // Arrows: L, T, R, B
231 
232 	UndoManager.prototype =
233 	{
234 		/**
235 		 * Process undo system regard keystrikes.
236 		 * @param {CKEDITOR.dom.event} event
237 		 */
238 		type : function( event )
239 		{
240 			var keystroke = event && event.data.getKey(),
241 				isModifierKey = keystroke in modifierKeyCodes,
242 				isEditingKey = keystroke in editingKeyCodes,
243 				wasEditingKey = this.lastKeystroke in editingKeyCodes,
244 				sameAsLastEditingKey = isEditingKey && keystroke == this.lastKeystroke,
245 				// Keystrokes which navigation through contents.
246 				isReset = keystroke in navigationKeyCodes,
247 				wasReset = this.lastKeystroke in navigationKeyCodes,
248 
249 				// Keystrokes which just introduce new contents.
250 				isContent = ( !isEditingKey && !isReset ),
251 
252 				// Create undo snap for every different modifier key.
253 				modifierSnapshot = ( isEditingKey && !sameAsLastEditingKey ),
254 				// Create undo snap on the following cases:
255 				// 1. Just start to type .
256 				// 2. Typing some content after a modifier.
257 				// 3. Typing some content after make a visible selection.
258 				startedTyping = !( isModifierKey || this.typing )
259 					|| ( isContent && ( wasEditingKey || wasReset ) );
260 
261 			if ( startedTyping || modifierSnapshot )
262 			{
263 				var beforeTypeImage = new Image( this.editor ),
264 					beforeTypeCount = this.snapshots.length;
265 
266 				// Use setTimeout, so we give the necessary time to the
267 				// browser to insert the character into the DOM.
268 				CKEDITOR.tools.setTimeout( function()
269 					{
270 						var currentSnapshot = this.editor.getSnapshot();
271 
272 						// In IE, we need to remove the expando attributes.
273 						if ( CKEDITOR.env.ie )
274 							currentSnapshot = currentSnapshot.replace( /\s+data-cke-expando=".*?"/g, '' );
275 
276 						// If changes have taken place, while not been captured yet (#8459),
277 						// compensate the snapshot.
278 						if ( beforeTypeImage.contents != currentSnapshot &&
279 							 beforeTypeCount == this.snapshots.length )
280 						{
281 							// It's safe to now indicate typing state.
282 							this.typing = true;
283 
284 							// This's a special save, with specified snapshot
285 							// and without auto 'fireChange'.
286 							if ( !this.save( false, beforeTypeImage, false ) )
287 								// Drop future snapshots.
288 								this.snapshots.splice( this.index + 1, this.snapshots.length - this.index - 1 );
289 
290 							this.hasUndo = true;
291 							this.hasRedo = false;
292 
293 							this.typesCount = 1;
294 							this.modifiersCount = 1;
295 
296 							this.onChange();
297 						}
298 					},
299 					0, this
300 				);
301 			}
302 
303 			this.lastKeystroke = keystroke;
304 
305 			// Create undo snap after typed too much (over 25 times).
306 			if ( isEditingKey )
307 			{
308 				this.typesCount = 0;
309 				this.modifiersCount++;
310 
311 				if ( this.modifiersCount > 25 )
312 				{
313 					this.save( false, null, false );
314 					this.modifiersCount = 1;
315 				}
316 			}
317 			else if ( !isReset )
318 			{
319 				this.modifiersCount = 0;
320 				this.typesCount++;
321 
322 				if ( this.typesCount > 25 )
323 				{
324 					this.save( false, null, false );
325 					this.typesCount = 1;
326 				}
327 			}
328 
329 		},
330 
331 		reset : function()	// Reset the undo stack.
332 		{
333 			/**
334 			 * Remember last pressed key.
335 			 */
336 			this.lastKeystroke = 0;
337 
338 			/**
339 			 * Stack for all the undo and redo snapshots, they're always created/removed
340 			 * in consistency.
341 			 */
342 			this.snapshots = [];
343 
344 			/**
345 			 * Current snapshot history index.
346 			 */
347 			this.index = -1;
348 
349 			this.limit = this.editor.config.undoStackSize || 20;
350 
351 			this.currentImage = null;
352 
353 			this.hasUndo = false;
354 			this.hasRedo = false;
355 
356 			this.resetType();
357 		},
358 
359 		/**
360 		 * Reset all states about typing.
361 		 * @see  UndoManager.type
362 		 */
363 		resetType : function()
364 		{
365 			this.typing = false;
366 			delete this.lastKeystroke;
367 			this.typesCount = 0;
368 			this.modifiersCount = 0;
369 		},
370 		fireChange : function()
371 		{
372 			this.hasUndo = !!this.getNextImage( true );
373 			this.hasRedo = !!this.getNextImage( false );
374 			// Reset typing
375 			this.resetType();
376 			this.onChange();
377 		},
378 
379 		/**
380 		 * Save a snapshot of document image for later retrieve.
381 		 */
382 		save : function( onContentOnly, image, autoFireChange )
383 		{
384 			var snapshots = this.snapshots;
385 
386 			// Get a content image.
387 			if ( !image )
388 				image = new Image( this.editor );
389 
390 			// Do nothing if it was not possible to retrieve an image.
391 			if ( image.contents === false )
392 				return false;
393 
394 			// Check if this is a duplicate. In such case, do nothing.
395 			if ( this.currentImage && image.equals( this.currentImage, onContentOnly ) )
396 				return false;
397 
398 			// Drop future snapshots.
399 			snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );
400 
401 			// If we have reached the limit, remove the oldest one.
402 			if ( snapshots.length == this.limit )
403 				snapshots.shift();
404 
405 			// Add the new image, updating the current index.
406 			this.index = snapshots.push( image ) - 1;
407 
408 			this.currentImage = image;
409 
410 			if ( autoFireChange !== false )
411 				this.fireChange();
412 			return true;
413 		},
414 
415 		restoreImage : function( image )
416 		{
417 			// Bring editor focused to restore selection.
418 			var editor = this.editor,
419 				sel;
420 
421 			if ( image.bookmarks )
422 			{
423 				editor.focus();
424 				// Retrieve the selection beforehand. (#8324)
425 				sel = editor.getSelection();
426 			}
427 
428 			this.editor.loadSnapshot( image.contents );
429 
430 			if ( image.bookmarks )
431 				sel.selectBookmarks( image.bookmarks );
432 			else if ( CKEDITOR.env.ie )
433 			{
434 				// IE BUG: If I don't set the selection to *somewhere* after setting
435 				// document contents, then IE would create an empty paragraph at the bottom
436 				// the next time the document is modified.
437 				var $range = this.editor.document.getBody().$.createTextRange();
438 				$range.collapse( true );
439 				$range.select();
440 			}
441 
442 			this.index = image.index;
443 
444 			// Update current image with the actual editor
445 			// content, since actualy content may differ from
446 			// the original snapshot due to dom change. (#4622)
447 			this.update();
448 			this.fireChange();
449 		},
450 
451 		// Get the closest available image.
452 		getNextImage : function( isUndo )
453 		{
454 			var snapshots = this.snapshots,
455 				currentImage = this.currentImage,
456 				image, i;
457 
458 			if ( currentImage )
459 			{
460 				if ( isUndo )
461 				{
462 					for ( i = this.index - 1 ; i >= 0 ; i-- )
463 					{
464 						image = snapshots[ i ];
465 						if ( !currentImage.equals( image, true ) )
466 						{
467 							image.index = i;
468 							return image;
469 						}
470 					}
471 				}
472 				else
473 				{
474 					for ( i = this.index + 1 ; i < snapshots.length ; i++ )
475 					{
476 						image = snapshots[ i ];
477 						if ( !currentImage.equals( image, true ) )
478 						{
479 							image.index = i;
480 							return image;
481 						}
482 					}
483 				}
484 			}
485 
486 			return null;
487 		},
488 
489 		/**
490 		 * Check the current redo state.
491 		 * @return {Boolean} Whether the document has previous state to
492 		 *		retrieve.
493 		 */
494 		redoable : function()
495 		{
496 			return this.enabled && this.hasRedo;
497 		},
498 
499 		/**
500 		 * Check the current undo state.
501 		 * @return {Boolean} Whether the document has future state to restore.
502 		 */
503 		undoable : function()
504 		{
505 			return this.enabled && this.hasUndo;
506 		},
507 
508 		/**
509 		 * Perform undo on current index.
510 		 */
511 		undo : function()
512 		{
513 			if ( this.undoable() )
514 			{
515 				this.save( true );
516 
517 				var image = this.getNextImage( true );
518 				if ( image )
519 					return this.restoreImage( image ), true;
520 			}
521 
522 			return false;
523 		},
524 
525 		/**
526 		 * Perform redo on current index.
527 		 */
528 		redo : function()
529 		{
530 			if ( this.redoable() )
531 			{
532 				// Try to save. If no changes have been made, the redo stack
533 				// will not change, so it will still be redoable.
534 				this.save( true );
535 
536 				// If instead we had changes, we can't redo anymore.
537 				if ( this.redoable() )
538 				{
539 					var image = this.getNextImage( false );
540 					if ( image )
541 						return this.restoreImage( image ), true;
542 				}
543 			}
544 
545 			return false;
546 		},
547 
548 		/**
549 		 * Update the last snapshot of the undo stack with the current editor content.
550 		 */
551 		update : function()
552 		{
553 			this.snapshots.splice( this.index, 1, ( this.currentImage = new Image( this.editor ) ) );
554 		}
555 	};
556 })();
557 
558 /**
559  * The number of undo steps to be saved. The higher this setting value the more
560  * memory is used for it.
561  * @name CKEDITOR.config.undoStackSize
562  * @type Number
563  * @default 20
564  * @example
565  * config.undoStackSize = 50;
566  */
567 
568 /**
569  * Fired when the editor is about to save an undo snapshot. This event can be
570  * fired by plugins and customizations to make the editor saving undo snapshots.
571  * @name CKEDITOR.editor#saveSnapshot
572  * @event
573  */
574 
575 /**
576  * Fired before an undo image is to be taken. An undo image represents the
577  * editor state at some point. It's saved into an undo store, so the editor is
578  * able to recover the editor state on undo and redo operations.
579  * @name CKEDITOR.editor#beforeUndoImage
580  * @since 3.5.3
581  * @see CKEDITOR.editor#afterUndoImage
582  * @event
583  */
584 
585 /**
586  * Fired after an undo image is taken. An undo image represents the
587  * editor state at some point. It's saved into an undo store, so the editor is
588  * able to recover the editor state on undo and redo operations.
589  * @name CKEDITOR.editor#afterUndoImage
590  * @since 3.5.3
591  * @see CKEDITOR.editor#beforeUndoImage
592  * @event
593  */
594