JasonWoof Got questions, comments, patches, etc.? Contact Jason Woofenden
vanilla ckeditor-3.0
[ckeditor.git] / _source / plugins / undo / plugin.js
diff --git a/_source/plugins/undo/plugin.js b/_source/plugins/undo/plugin.js
new file mode 100644 (file)
index 0000000..e013a9d
--- /dev/null
@@ -0,0 +1,490 @@
+/*\r
+Copyright (c) 2003-2009, 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
+\r
+       // Gets a snapshot image which represent the current document status.\r
+       function Image( editor )\r
+       {\r
+               var selection = editor.getSelection();\r
+\r
+               this.contents   = editor.getSnapshot();\r
+               this.bookmarks  = selection && selection.createBookmarks2( true );\r
+\r
+               // In IE, we need to remove the expando attributes.\r
+               if ( CKEDITOR.env.ie )\r
+                       this.contents = this.contents.replace( /\s+_cke_expando=".*?"/g, '' );\r
+       }\r
+\r
+       Image.prototype =\r
+       {\r
+               equals : function( otherImage, contentOnly )\r
+               {\r
+                       if ( this.contents != otherImage.contents )\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
+       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.getKeystroke(),\r
+\r
+                               // Backspace, Delete\r
+                               modifierCodes = { 8:1, 46:1 },\r
+                               // Keystrokes which will modify the contents.\r
+                               isModifier = keystroke in modifierCodes,\r
+                               wasModifier = this.lastKeystroke in modifierCodes,\r
+                               lastWasSameModifier = isModifier && keystroke == this.lastKeystroke,\r
+\r
+                               // Arrows: L, T, R, B\r
+                               resetTypingCodes = { 37:1, 38:1, 39:1, 40:1 },\r
+                               // Keystrokes which navigation through contents.\r
+                               isReset = keystroke in resetTypingCodes,\r
+                               wasReset = this.lastKeystroke in resetTypingCodes,\r
+\r
+                               // Keystrokes which just introduce new contents.\r
+                               isContent = ( !isModifier && !isReset ),\r
+\r
+                               // Create undo snap for every different modifier key.\r
+                               modifierSnapshot = ( isModifier && !lastWasSameModifier ),\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 = !this.typing\r
+                                       || ( isContent && ( wasModifier || 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+_cke_expando=".*?"/g, '' );\r
+\r
+                                               if ( beforeTypeImage.contents != currentSnapshot )\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
+                       // Create undo snap after typed too much (over 25 times).\r
+                       if ( isModifier )\r
+                       {\r
+                               this.typesCount = 0;\r
+                               this.modifiersCount++;\r
+\r
+                               if ( this.modifiersCount > 25 )\r
+                               {\r
+                                       this.save();\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();\r
+                                       this.typesCount = 1;\r
+                               }\r
+                       }\r
+\r
+                       this.typing = true;\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;\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
+                       // 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
+                       this.currentImage = image;\r
+\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
+\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
+CKEDITOR.config.undoStackSize = 20;\r