--- /dev/null
+/*\r
+Copyright (c) 2003-2011, CKSource - Frederico Knabben. All rights reserved.\r
+For licensing, see LICENSE.html or http://ckeditor.com/license\r
+*/\r
+\r
+/**\r
+ * @fileOverview Undo/Redo system for saving shapshot for document modification\r
+ * and other recordable changes.\r
+ */\r
+\r
+(function()\r
+{\r
+ CKEDITOR.plugins.add( 'undo',\r
+ {\r
+ requires : [ 'selection', 'wysiwygarea' ],\r
+\r
+ init : function( editor )\r
+ {\r
+ var undoManager = new UndoManager( editor );\r
+\r
+ var undoCommand = editor.addCommand( 'undo',\r
+ {\r
+ exec : function()\r
+ {\r
+ if ( undoManager.undo() )\r
+ {\r
+ editor.selectionChange();\r
+ this.fire( 'afterUndo' );\r
+ }\r
+ },\r
+ state : CKEDITOR.TRISTATE_DISABLED,\r
+ canUndo : false\r
+ });\r
+\r
+ var redoCommand = editor.addCommand( 'redo',\r
+ {\r
+ exec : function()\r
+ {\r
+ if ( undoManager.redo() )\r
+ {\r
+ editor.selectionChange();\r
+ this.fire( 'afterRedo' );\r
+ }\r
+ },\r
+ state : CKEDITOR.TRISTATE_DISABLED,\r
+ canUndo : false\r
+ });\r
+\r
+ undoManager.onChange = function()\r
+ {\r
+ undoCommand.setState( undoManager.undoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );\r
+ redoCommand.setState( undoManager.redoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );\r
+ };\r
+\r
+ function recordCommand( event )\r
+ {\r
+ // If the command hasn't been marked to not support undo.\r
+ if ( undoManager.enabled && event.data.command.canUndo !== false )\r
+ undoManager.save();\r
+ }\r
+\r
+ // We'll save snapshots before and after executing a command.\r
+ editor.on( 'beforeCommandExec', recordCommand );\r
+ editor.on( 'afterCommandExec', recordCommand );\r
+\r
+ // Save snapshots before doing custom changes.\r
+ editor.on( 'saveSnapshot', function()\r
+ {\r
+ undoManager.save();\r
+ });\r
+\r
+ // Registering keydown on every document recreation.(#3844)\r
+ editor.on( 'contentDom', function()\r
+ {\r
+ editor.document.on( 'keydown', function( event )\r
+ {\r
+ // Do not capture CTRL hotkeys.\r
+ if ( !event.data.$.ctrlKey && !event.data.$.metaKey )\r
+ undoManager.type( event );\r
+ });\r
+ });\r
+\r
+ // Always save an undo snapshot - the previous mode might have\r
+ // changed editor contents.\r
+ editor.on( 'beforeModeUnload', function()\r
+ {\r
+ editor.mode == 'wysiwyg' && undoManager.save( true );\r
+ });\r
+\r
+ // Make the undo manager available only in wysiwyg mode.\r
+ editor.on( 'mode', function()\r
+ {\r
+ undoManager.enabled = editor.mode == 'wysiwyg';\r
+ undoManager.onChange();\r
+ });\r
+\r
+ editor.ui.addButton( 'Undo',\r
+ {\r
+ label : editor.lang.undo,\r
+ command : 'undo'\r
+ });\r
+\r
+ editor.ui.addButton( 'Redo',\r
+ {\r
+ label : editor.lang.redo,\r
+ command : 'redo'\r
+ });\r
+\r
+ editor.resetUndo = function()\r
+ {\r
+ // Reset the undo stack.\r
+ undoManager.reset();\r
+\r
+ // Create the first image.\r
+ editor.fire( 'saveSnapshot' );\r
+ };\r
+\r
+ /**\r
+ * Update the undo stacks with any subsequent DOM changes after this call.\r
+ * @name CKEDITOR.editor#updateUndo\r
+ * @example\r
+ * function()\r
+ * {\r
+ * editor.fire( 'updateSnapshot' );\r
+ * ...\r
+ * // Ask to include subsequent (in this call stack) DOM changes to be\r
+ * // considered as part of the first snapshot.\r
+ * editor.fire( 'updateSnapshot' );\r
+ * editor.document.body.append(...);\r
+ * ...\r
+ * }\r
+ */\r
+ editor.on( 'updateSnapshot', function()\r
+ {\r
+ if ( undoManager.currentImage && new Image( editor ).equals( undoManager.currentImage ) )\r
+ setTimeout( function() { undoManager.update(); }, 0 );\r
+ });\r
+ }\r
+ });\r
+\r
+ CKEDITOR.plugins.undo = {};\r
+\r
+ /**\r
+ * Undo snapshot which represents the current document status.\r
+ * @name CKEDITOR.plugins.undo.Image\r
+ * @param editor The editor instance on which the image is created.\r
+ */\r
+ var Image = CKEDITOR.plugins.undo.Image = function( editor )\r
+ {\r
+ this.editor = editor;\r
+\r
+ editor.fire( 'beforeUndoImage' );\r
+\r
+ var contents = editor.getSnapshot(),\r
+ selection = contents && editor.getSelection();\r
+\r
+ // In IE, we need to remove the expando attributes.\r
+ CKEDITOR.env.ie && contents && ( contents = contents.replace( /\s+data-cke-expando=".*?"/g, '' ) );\r
+\r
+ this.contents = contents;\r
+ this.bookmarks = selection && selection.createBookmarks2( true );\r
+\r
+ editor.fire( 'afterUndoImage' );\r
+ };\r
+\r
+ // Attributes that browser may changing them when setting via innerHTML.\r
+ var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;\r
+\r
+ Image.prototype =\r
+ {\r
+ equals : function( otherImage, contentOnly )\r
+ {\r
+\r
+ var thisContents = this.contents,\r
+ otherContents = otherImage.contents;\r
+\r
+ // For IE6/7 : Comparing only the protected attribute values but not the original ones.(#4522)\r
+ if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.ie6Compat ) )\r
+ {\r
+ thisContents = thisContents.replace( protectedAttrs, '' );\r
+ otherContents = otherContents.replace( protectedAttrs, '' );\r
+ }\r
+\r
+ if ( thisContents != otherContents )\r
+ return false;\r
+\r
+ if ( contentOnly )\r
+ return true;\r
+\r
+ var bookmarksA = this.bookmarks,\r
+ bookmarksB = otherImage.bookmarks;\r
+\r
+ if ( bookmarksA || bookmarksB )\r
+ {\r
+ if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )\r
+ return false;\r
+\r
+ for ( var i = 0 ; i < bookmarksA.length ; i++ )\r
+ {\r
+ var bookmarkA = bookmarksA[ i ],\r
+ bookmarkB = bookmarksB[ i ];\r
+\r
+ if (\r
+ bookmarkA.startOffset != bookmarkB.startOffset ||\r
+ bookmarkA.endOffset != bookmarkB.endOffset ||\r
+ !CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) ||\r
+ !CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) )\r
+ {\r
+ return false;\r
+ }\r
+ }\r
+ }\r
+\r
+ return true;\r
+ }\r
+ };\r
+\r
+ /**\r
+ * @constructor Main logic for Redo/Undo feature.\r
+ */\r
+ function UndoManager( editor )\r
+ {\r
+ this.editor = editor;\r
+\r
+ // Reset the undo stack.\r
+ this.reset();\r
+ }\r
+\r
+\r
+ var editingKeyCodes = { /*Backspace*/ 8:1, /*Delete*/ 46:1 },\r
+ modifierKeyCodes = { /*Shift*/ 16:1, /*Ctrl*/ 17:1, /*Alt*/ 18:1 },\r
+ navigationKeyCodes = { 37:1, 38:1, 39:1, 40:1 }; // Arrows: L, T, R, B\r
+\r
+ UndoManager.prototype =\r
+ {\r
+ /**\r
+ * Process undo system regard keystrikes.\r
+ * @param {CKEDITOR.dom.event} event\r
+ */\r
+ type : function( event )\r
+ {\r
+ var keystroke = event && event.data.getKey(),\r
+ isModifierKey = keystroke in modifierKeyCodes,\r
+ isEditingKey = keystroke in editingKeyCodes,\r
+ wasEditingKey = this.lastKeystroke in editingKeyCodes,\r
+ sameAsLastEditingKey = isEditingKey && keystroke == this.lastKeystroke,\r
+ // Keystrokes which navigation through contents.\r
+ isReset = keystroke in navigationKeyCodes,\r
+ wasReset = this.lastKeystroke in navigationKeyCodes,\r
+\r
+ // Keystrokes which just introduce new contents.\r
+ isContent = ( !isEditingKey && !isReset ),\r
+\r
+ // Create undo snap for every different modifier key.\r
+ modifierSnapshot = ( isEditingKey && !sameAsLastEditingKey ),\r
+ // Create undo snap on the following cases:\r
+ // 1. Just start to type .\r
+ // 2. Typing some content after a modifier.\r
+ // 3. Typing some content after make a visible selection.\r
+ startedTyping = !( isModifierKey || this.typing )\r
+ || ( isContent && ( wasEditingKey || wasReset ) );\r
+\r
+ if ( startedTyping || modifierSnapshot )\r
+ {\r
+ var beforeTypeImage = new Image( this.editor );\r
+\r
+ // Use setTimeout, so we give the necessary time to the\r
+ // browser to insert the character into the DOM.\r
+ CKEDITOR.tools.setTimeout( function()\r
+ {\r
+ var currentSnapshot = this.editor.getSnapshot();\r
+\r
+ // In IE, we need to remove the expando attributes.\r
+ if ( CKEDITOR.env.ie )\r
+ currentSnapshot = currentSnapshot.replace( /\s+data-cke-expando=".*?"/g, '' );\r
+\r
+ if ( beforeTypeImage.contents != currentSnapshot )\r
+ {\r
+ // It's safe to now indicate typing state.\r
+ this.typing = true;\r
+\r
+ // This's a special save, with specified snapshot\r
+ // and without auto 'fireChange'.\r
+ if ( !this.save( false, beforeTypeImage, false ) )\r
+ // Drop future snapshots.\r
+ this.snapshots.splice( this.index + 1, this.snapshots.length - this.index - 1 );\r
+\r
+ this.hasUndo = true;\r
+ this.hasRedo = false;\r
+\r
+ this.typesCount = 1;\r
+ this.modifiersCount = 1;\r
+\r
+ this.onChange();\r
+ }\r
+ },\r
+ 0, this\r
+ );\r
+ }\r
+\r
+ this.lastKeystroke = keystroke;\r
+\r
+ // Create undo snap after typed too much (over 25 times).\r
+ if ( isEditingKey )\r
+ {\r
+ this.typesCount = 0;\r
+ this.modifiersCount++;\r
+\r
+ if ( this.modifiersCount > 25 )\r
+ {\r
+ this.save( false, null, false );\r
+ this.modifiersCount = 1;\r
+ }\r
+ }\r
+ else if ( !isReset )\r
+ {\r
+ this.modifiersCount = 0;\r
+ this.typesCount++;\r
+\r
+ if ( this.typesCount > 25 )\r
+ {\r
+ this.save( false, null, false );\r
+ this.typesCount = 1;\r
+ }\r
+ }\r
+\r
+ },\r
+\r
+ reset : function() // Reset the undo stack.\r
+ {\r
+ /**\r
+ * Remember last pressed key.\r
+ */\r
+ this.lastKeystroke = 0;\r
+\r
+ /**\r
+ * Stack for all the undo and redo snapshots, they're always created/removed\r
+ * in consistency.\r
+ */\r
+ this.snapshots = [];\r
+\r
+ /**\r
+ * Current snapshot history index.\r
+ */\r
+ this.index = -1;\r
+\r
+ this.limit = this.editor.config.undoStackSize || 20;\r
+\r
+ this.currentImage = null;\r
+\r
+ this.hasUndo = false;\r
+ this.hasRedo = false;\r
+\r
+ this.resetType();\r
+ },\r
+\r
+ /**\r
+ * Reset all states about typing.\r
+ * @see UndoManager.type\r
+ */\r
+ resetType : function()\r
+ {\r
+ this.typing = false;\r
+ delete this.lastKeystroke;\r
+ this.typesCount = 0;\r
+ this.modifiersCount = 0;\r
+ },\r
+ fireChange : function()\r
+ {\r
+ this.hasUndo = !!this.getNextImage( true );\r
+ this.hasRedo = !!this.getNextImage( false );\r
+ // Reset typing\r
+ this.resetType();\r
+ this.onChange();\r
+ },\r
+\r
+ /**\r
+ * Save a snapshot of document image for later retrieve.\r
+ */\r
+ save : function( onContentOnly, image, autoFireChange )\r
+ {\r
+ var snapshots = this.snapshots;\r
+\r
+ // Get a content image.\r
+ if ( !image )\r
+ image = new Image( this.editor );\r
+\r
+ // Do nothing if it was not possible to retrieve an image.\r
+ if ( image.contents === false )\r
+ return false;\r
+\r
+ // Check if this is a duplicate. In such case, do nothing.\r
+ if ( this.currentImage && image.equals( this.currentImage, onContentOnly ) )\r
+ return false;\r
+\r
+ // Drop future snapshots.\r
+ snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );\r
+\r
+ // If we have reached the limit, remove the oldest one.\r
+ if ( snapshots.length == this.limit )\r
+ snapshots.shift();\r
+\r
+ // Add the new image, updating the current index.\r
+ this.index = snapshots.push( image ) - 1;\r
+\r
+ this.currentImage = image;\r
+\r
+ if ( autoFireChange !== false )\r
+ this.fireChange();\r
+ return true;\r
+ },\r
+\r
+ restoreImage : function( image )\r
+ {\r
+ this.editor.loadSnapshot( image.contents );\r
+\r
+ if ( image.bookmarks )\r
+ this.editor.getSelection().selectBookmarks( image.bookmarks );\r
+ else if ( CKEDITOR.env.ie )\r
+ {\r
+ // IE BUG: If I don't set the selection to *somewhere* after setting\r
+ // document contents, then IE would create an empty paragraph at the bottom\r
+ // the next time the document is modified.\r
+ var $range = this.editor.document.getBody().$.createTextRange();\r
+ $range.collapse( true );\r
+ $range.select();\r
+ }\r
+\r
+ this.index = image.index;\r
+\r
+ // Update current image with the actual editor\r
+ // content, since actualy content may differ from\r
+ // the original snapshot due to dom change. (#4622)\r
+ this.update();\r
+ this.fireChange();\r
+ },\r
+\r
+ // Get the closest available image.\r
+ getNextImage : function( isUndo )\r
+ {\r
+ var snapshots = this.snapshots,\r
+ currentImage = this.currentImage,\r
+ image, i;\r
+\r
+ if ( currentImage )\r
+ {\r
+ if ( isUndo )\r
+ {\r
+ for ( i = this.index - 1 ; i >= 0 ; i-- )\r
+ {\r
+ image = snapshots[ i ];\r
+ if ( !currentImage.equals( image, true ) )\r
+ {\r
+ image.index = i;\r
+ return image;\r
+ }\r
+ }\r
+ }\r
+ else\r
+ {\r
+ for ( i = this.index + 1 ; i < snapshots.length ; i++ )\r
+ {\r
+ image = snapshots[ i ];\r
+ if ( !currentImage.equals( image, true ) )\r
+ {\r
+ image.index = i;\r
+ return image;\r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ return null;\r
+ },\r
+\r
+ /**\r
+ * Check the current redo state.\r
+ * @return {Boolean} Whether the document has previous state to\r
+ * retrieve.\r
+ */\r
+ redoable : function()\r
+ {\r
+ return this.enabled && this.hasRedo;\r
+ },\r
+\r
+ /**\r
+ * Check the current undo state.\r
+ * @return {Boolean} Whether the document has future state to restore.\r
+ */\r
+ undoable : function()\r
+ {\r
+ return this.enabled && this.hasUndo;\r
+ },\r
+\r
+ /**\r
+ * Perform undo on current index.\r
+ */\r
+ undo : function()\r
+ {\r
+ if ( this.undoable() )\r
+ {\r
+ this.save( true );\r
+\r
+ var image = this.getNextImage( true );\r
+ if ( image )\r
+ return this.restoreImage( image ), true;\r
+ }\r
+\r
+ return false;\r
+ },\r
+\r
+ /**\r
+ * Perform redo on current index.\r
+ */\r
+ redo : function()\r
+ {\r
+ if ( this.redoable() )\r
+ {\r
+ // Try to save. If no changes have been made, the redo stack\r
+ // will not change, so it will still be redoable.\r
+ this.save( true );\r
+\r
+ // If instead we had changes, we can't redo anymore.\r
+ if ( this.redoable() )\r
+ {\r
+ var image = this.getNextImage( false );\r
+ if ( image )\r
+ return this.restoreImage( image ), true;\r
+ }\r
+ }\r
+\r
+ return false;\r
+ },\r
+\r
+ /**\r
+ * Update the last snapshot of the undo stack with the current editor content.\r
+ */\r
+ update : function()\r
+ {\r
+ this.snapshots.splice( this.index, 1, ( this.currentImage = new Image( this.editor ) ) );\r
+ }\r
+ };\r
+})();\r
+\r
+/**\r
+ * The number of undo steps to be saved. The higher this setting value the more\r
+ * memory is used for it.\r
+ * @type Number\r
+ * @default 20\r
+ * @example\r
+ * config.undoStackSize = 50;\r
+ */\r
+\r
+/**\r
+ * Fired when the editor is about to save an undo snapshot. This event can be\r
+ * fired by plugins and customizations to make the editor saving undo snapshots.\r
+ * @name CKEDITOR.editor#saveSnapshot\r
+ * @event\r
+ */\r
+\r
+/**\r
+ * Fired before an undo image is to be taken. An undo image represents the\r
+ * editor state at some point. It's saved into an undo store, so the editor is\r
+ * able to recover the editor state on undo and redo operations.\r
+ * @name CKEDITOR.editor#beforeUndoImage\r
+ * @since 3.5.3\r
+ * @see CKEDITOR.editor#afterUndoImage\r
+ * @event\r
+ */\r
+\r
+/**\r
+ * Fired after an undo image is taken. An undo image represents the\r
+ * editor state at some point. It's saved into an undo store, so the editor is\r
+ * able to recover the editor state on undo and redo operations.\r
+ * @name CKEDITOR.editor#afterUndoImage\r
+ * @since 3.5.3\r
+ * @see CKEDITOR.editor#beforeUndoImage\r
+ * @event\r
+ */\r