﻿/*

Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.

For licensing, see LICENSE.html or http://ckeditor.com/license

*/



/**

 * @fileOverview Undo/Redo system for saving shapshot for document modification

 *		and other recordable changes.

 */



(function()

{

	CKEDITOR.plugins.add( 'undo',

	{

		requires : [ 'selection', 'wysiwygarea' ],



		init : function( editor )

		{

			var undoManager = new UndoManager( editor );



			var undoCommand = editor.addCommand( 'undo',

				{

					exec : function()

					{

						if ( undoManager.undo() )

						{

							editor.selectionChange();

							this.fire( 'afterUndo' );

						}

					},

					state : CKEDITOR.TRISTATE_DISABLED,

					canUndo : false

				});



			var redoCommand = editor.addCommand( 'redo',

				{

					exec : function()

					{

						if ( undoManager.redo() )

						{

							editor.selectionChange();

							this.fire( 'afterRedo' );

						}

					},

					state : CKEDITOR.TRISTATE_DISABLED,

					canUndo : false

				});



			undoManager.onChange = function()

			{

				undoCommand.setState( undoManager.undoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );

				redoCommand.setState( undoManager.redoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );

			};



			function recordCommand( event )

			{

				// If the command hasn't been marked to not support undo.

				if ( undoManager.enabled && event.data.command.canUndo !== false )

					undoManager.save();

			}



			// We'll save snapshots before and after executing a command.

			editor.on( 'beforeCommandExec', recordCommand );

			editor.on( 'afterCommandExec', recordCommand );



			// Save snapshots before doing custom changes.

			editor.on( 'saveSnapshot', function()

				{

					undoManager.save();

				});



			// Registering keydown on every document recreation.(#3844)

			editor.on( 'contentDom', function()

				{

					editor.document.on( 'keydown', function( event )

						{

							// Do not capture CTRL hotkeys.

							if ( !event.data.$.ctrlKey && !event.data.$.metaKey )

								undoManager.type( event );

						});

				});



			// Always save an undo snapshot - the previous mode might have

			// changed editor contents.

			editor.on( 'beforeModeUnload', function()

				{

					editor.mode == 'wysiwyg' && undoManager.save( true );

				});



			// Make the undo manager available only in wysiwyg mode.

			editor.on( 'mode', function()

				{

					undoManager.enabled = editor.mode == 'wysiwyg';

					undoManager.onChange();

				});



			editor.ui.addButton( 'Undo',

				{

					label : editor.lang.undo,

					command : 'undo'

				});



			editor.ui.addButton( 'Redo',

				{

					label : editor.lang.redo,

					command : 'redo'

				});



			editor.resetUndo = function()

			{

				// Reset the undo stack.

				undoManager.reset();



				// Create the first image.

				editor.fire( 'saveSnapshot' );

			};

		}

	});



	// Gets a snapshot image which represent the current document status.

	function Image( editor )

	{

		var contents	= editor.getSnapshot(),

			selection	= contents && editor.getSelection();



		// In IE, we need to remove the expando attributes.

		CKEDITOR.env.ie && contents && ( contents = contents.replace( /\s+_cke_expando=".*?"/g, '' ) );



		this.contents	= contents;

		this.bookmarks	= selection && selection.createBookmarks2( true );

	}



	// Attributes that browser may changing them when setting via innerHTML.

	var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;



	Image.prototype =

	{

		equals : function( otherImage, contentOnly )

		{

			var thisContents = this.contents,

				otherContents = otherImage.contents;



			// For IE6/7 : Comparing only the protected attribute values but not the original ones.(#4522)

			if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) )

			{

				thisContents = thisContents.replace( protectedAttrs, '' );

				otherContents = otherContents.replace( protectedAttrs, '' );

			}



			if ( thisContents != otherContents )

				return false;



			if ( contentOnly )

				return true;



			var bookmarksA = this.bookmarks,

				bookmarksB = otherImage.bookmarks;



			if ( bookmarksA || bookmarksB )

			{

				if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )

					return false;



				for ( var i = 0 ; i < bookmarksA.length ; i++ )

				{

					var bookmarkA = bookmarksA[ i ],

						bookmarkB = bookmarksB[ i ];



					if (

						bookmarkA.startOffset != bookmarkB.startOffset ||

						bookmarkA.endOffset != bookmarkB.endOffset ||

						!CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) ||

						!CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) )

					{

						return false;

					}

				}

			}



			return true;

		}

	};



	/**

	 * @constructor Main logic for Redo/Undo feature.

	 */

	function UndoManager( editor )

	{

		this.editor = editor;



		// Reset the undo stack.

		this.reset();

	}





	var editingKeyCodes = { /*Backspace*/ 8:1, /*Delete*/ 46:1 },

		modifierKeyCodes = { /*Shift*/ 16:1, /*Ctrl*/ 17:1, /*Alt*/ 18:1 },

		navigationKeyCodes = { 37:1, 38:1, 39:1, 40:1 };  // Arrows: L, T, R, B



	UndoManager.prototype =

	{

		/**

		 * Process undo system regard keystrikes.

		 * @param {CKEDITOR.dom.event} event

		 */

		type : function( event )

		{

			var keystroke = event && event.data.getKey(),

				isModifierKey = keystroke in modifierKeyCodes,

				isEditingKey = keystroke in editingKeyCodes,

				wasEditingKey = this.lastKeystroke in editingKeyCodes,

				sameAsLastEditingKey = isEditingKey && keystroke == this.lastKeystroke,

				// Keystrokes which navigation through contents.

				isReset = keystroke in navigationKeyCodes,

				wasReset = this.lastKeystroke in navigationKeyCodes,



				// Keystrokes which just introduce new contents.

				isContent = ( !isEditingKey && !isReset ),



				// Create undo snap for every different modifier key.

				modifierSnapshot = ( isEditingKey && !sameAsLastEditingKey ),

				// Create undo snap on the following cases:

				// 1. Just start to type .

				// 2. Typing some content after a modifier.

				// 3. Typing some content after make a visible selection.

				startedTyping = !( isModifierKey || this.typing )

					|| ( isContent && ( wasEditingKey || wasReset ) );



			if ( startedTyping || modifierSnapshot )

			{

				var beforeTypeImage = new Image( this.editor );



				// Use setTimeout, so we give the necessary time to the

				// browser to insert the character into the DOM.

				CKEDITOR.tools.setTimeout( function()

					{

						var currentSnapshot = this.editor.getSnapshot();



						// In IE, we need to remove the expando attributes.

						if ( CKEDITOR.env.ie )

							currentSnapshot = currentSnapshot.replace( /\s+_cke_expando=".*?"/g, '' );



						if ( beforeTypeImage.contents != currentSnapshot )

						{

							// It's safe to now indicate typing state.

							this.typing = true;



							// This's a special save, with specified snapshot

							// and without auto 'fireChange'.

							if ( !this.save( false, beforeTypeImage, false ) )

								// Drop future snapshots.

								this.snapshots.splice( this.index + 1, this.snapshots.length - this.index - 1 );



							this.hasUndo = true;

							this.hasRedo = false;



							this.typesCount = 1;

							this.modifiersCount = 1;



							this.onChange();

						}

					},

					0, this

				);

			}



			this.lastKeystroke = keystroke;



			// Create undo snap after typed too much (over 25 times).

			if ( isEditingKey )

			{

				this.typesCount = 0;

				this.modifiersCount++;



				if ( this.modifiersCount > 25 )

				{

					this.save( false, null, false );

					this.modifiersCount = 1;

				}

			}

			else if ( !isReset )

			{

				this.modifiersCount = 0;

				this.typesCount++;



				if ( this.typesCount > 25 )

				{

					this.save( false, null, false );

					this.typesCount = 1;

				}

			}



		},



		reset : function()	// Reset the undo stack.

		{

			/**

			 * Remember last pressed key.

			 */

			this.lastKeystroke = 0;



			/**

			 * Stack for all the undo and redo snapshots, they're always created/removed

			 * in consistency.

			 */

			this.snapshots = [];



			/**

			 * Current snapshot history index.

			 */

			this.index = -1;



			this.limit = this.editor.config.undoStackSize;



			this.currentImage = null;



			this.hasUndo = false;

			this.hasRedo = false;



			this.resetType();

		},



		/**

		 * Reset all states about typing.

		 * @see  UndoManager.type

		 */

		resetType : function()

		{

			this.typing = false;

			delete this.lastKeystroke;

			this.typesCount = 0;

			this.modifiersCount = 0;

		},

		fireChange : function()

		{

			this.hasUndo = !!this.getNextImage( true );

			this.hasRedo = !!this.getNextImage( false );

			// Reset typing

			this.resetType();

			this.onChange();

		},



		/**

		 * Save a snapshot of document image for later retrieve.

		 */

		save : function( onContentOnly, image, autoFireChange )

		{

			var snapshots = this.snapshots;



			// Get a content image.

			if ( !image )

				image = new Image( this.editor );



			// Do nothing if it was not possible to retrieve an image.

			if ( image.contents === false )

				return false;



			// Check if this is a duplicate. In such case, do nothing.

			if ( this.currentImage && image.equals( this.currentImage, onContentOnly ) )

				return false;



			// Drop future snapshots.

			snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );



			// If we have reached the limit, remove the oldest one.

			if ( snapshots.length == this.limit )

				snapshots.shift();



			// Add the new image, updating the current index.

			this.index = snapshots.push( image ) - 1;



			this.currentImage = image;



			if ( autoFireChange !== false )

				this.fireChange();

			return true;

		},



		restoreImage : function( image )

		{

			this.editor.loadSnapshot( image.contents );



			if ( image.bookmarks )

				this.editor.getSelection().selectBookmarks( image.bookmarks );

			else if ( CKEDITOR.env.ie )

			{

				// IE BUG: If I don't set the selection to *somewhere* after setting

				// document contents, then IE would create an empty paragraph at the bottom

				// the next time the document is modified.

				var $range = this.editor.document.getBody().$.createTextRange();

				$range.collapse( true );

				$range.select();

			}



			this.index = image.index;



			// Update current image with the actual editor

			// content, since actualy content may differ from

			// the original snapshot due to dom change. (#4622)

			this.snapshots.splice( this.index, 1, ( this.currentImage =  new Image( this.editor ) ) );



			this.fireChange();

		},



		// Get the closest available image.

		getNextImage : function( isUndo )

		{

			var snapshots = this.snapshots,

				currentImage = this.currentImage,

				image, i;



			if ( currentImage )

			{

				if ( isUndo )

				{

					for ( i = this.index - 1 ; i >= 0 ; i-- )

					{

						image = snapshots[ i ];

						if ( !currentImage.equals( image, true ) )

						{

							image.index = i;

							return image;

						}

					}

				}

				else

				{

					for ( i = this.index + 1 ; i < snapshots.length ; i++ )

					{

						image = snapshots[ i ];

						if ( !currentImage.equals( image, true ) )

						{

							image.index = i;

							return image;

						}

					}

				}

			}



			return null;

		},



		/**

		 * Check the current redo state.

		 * @return {Boolean} Whether the document has previous state to

		 *		retrieve.

		 */

		redoable : function()

		{

			return this.enabled && this.hasRedo;

		},



		/**

		 * Check the current undo state.

		 * @return {Boolean} Whether the document has future state to restore.

		 */

		undoable : function()

		{

			return this.enabled && this.hasUndo;

		},



		/**

		 * Perform undo on current index.

		 */

		undo : function()

		{

			if ( this.undoable() )

			{

				this.save( true );



				var image = this.getNextImage( true );

				if ( image )

					return this.restoreImage( image ), true;

			}



			return false;

		},



		/**

		 * Perform redo on current index.

		 */

		redo : function()

		{

			if ( this.redoable() )

			{

				// Try to save. If no changes have been made, the redo stack

				// will not change, so it will still be redoable.

				this.save( true );



				// If instead we had changes, we can't redo anymore.

				if ( this.redoable() )

				{

					var image = this.getNextImage( false );

					if ( image )

						return this.restoreImage( image ), true;

				}

			}



			return false;

		}

	};

})();



/**

 * The number of undo steps to be saved. The higher this setting value the more

 * memory is used for it.

 * @type Number

 * @default 20

 * @example

 * config.undoStackSize = 50;

 */

CKEDITOR.config.undoStackSize = 20;



/**

 * Fired when the editor is about to save an undo snapshot. This event can be

 * fired by plugins and customizations to make the editor saving undo snapshots.

 * @name CKEDITOR.editor#saveSnapshot

 * @event

 */

