1 /* 2 Copyright (c) 2003-2009, 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 // Make the undo manager available only in wysiwyg mode. 67 editor.on( 'mode', function() 68 { 69 if ( editor.mode == 'wysiwyg' ) 70 { 71 if ( !undoManager.enabled ) 72 { 73 undoManager.enabled = true; 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(); 80 }); 81 82 // Being this the first call, let's get an undo snapshot. 83 if ( undoManager.index == -1 ) 84 undoManager.save(); 85 } 86 } 87 else 88 undoManager.enabled = false; 89 90 undoManager.onChange(); 91 }); 92 93 editor.ui.addButton( 'Undo', 94 { 95 label : editor.lang.undo, 96 command : 'undo' 97 }); 98 99 editor.ui.addButton( 'Redo', 100 { 101 label : editor.lang.redo, 102 command : 'redo' 103 }); 104 } 105 }); 106 107 // Gets a snapshot image which represent the current document status. 108 function Image( editor ) 109 { 110 var selection = editor.getSelection(); 111 112 this.contents = editor.getSnapshot(); 113 this.bookmarks = selection && selection.createBookmarks2( true ); 114 115 // In IE, we need to remove the expando attributes. 116 if ( CKEDITOR.env.ie ) 117 this.contents = this.contents.replace( /\s+_cke_expando=".*?"/g, '' ); 118 } 119 120 Image.prototype = 121 { 122 equals : function( otherImage, contentOnly ) 123 { 124 if ( this.contents != otherImage.contents ) 125 return false; 126 127 if ( contentOnly ) 128 return true; 129 130 var bookmarksA = this.bookmarks, 131 bookmarksB = otherImage.bookmarks; 132 133 if ( bookmarksA || bookmarksB ) 134 { 135 if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length ) 136 return false; 137 138 for ( var i = 0 ; i < bookmarksA.length ; i++ ) 139 { 140 var bookmarkA = bookmarksA[ i ], 141 bookmarkB = bookmarksB[ i ]; 142 143 if ( 144 bookmarkA.startOffset != bookmarkB.startOffset || 145 bookmarkA.endOffset != bookmarkB.endOffset || 146 !CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) || 147 !CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) ) 148 { 149 return false; 150 } 151 } 152 } 153 154 return true; 155 } 156 }; 157 158 /** 159 * @constructor Main logic for Redo/Undo feature. 160 */ 161 function UndoManager( editor ) 162 { 163 this.typesCount = 0; 164 165 this.editor = editor; 166 167 /** 168 * Stack for all the undo and redo snapshots, they're always created/removed 169 * in consistency. 170 */ 171 this.snapshots = []; 172 173 /** 174 * Current snapshot history index. 175 */ 176 this.index = -1; 177 178 this.limit = editor.config.undoStackSize; 179 } 180 181 UndoManager.prototype = 182 { 183 type : function() 184 { 185 if ( !this.typing ) 186 { 187 var beforeTypeImage = new Image( this.editor ); 188 189 // Use setTimeout, so we give the necessary time to the 190 // browser to insert the character into the DOM. 191 CKEDITOR.tools.setTimeout( function() 192 { 193 var currentSnapshot = this.editor.getSnapshot(); 194 195 // In IE, we need to remove the expando attributes. 196 if ( CKEDITOR.env.ie ) 197 currentSnapshot = currentSnapshot.replace( /\s+_cke_expando=".*?"/g, '' ); 198 199 if ( beforeTypeImage.contents != currentSnapshot ) 200 { 201 if ( !this.save( false, beforeTypeImage ) ) 202 { 203 // Drop future snapshots. 204 this.snapshots.splice( this.index + 1, this.snapshots.length - this.index - 1 ); 205 } 206 207 this.hasUndo = true; 208 this.hasRedo = false; 209 210 this.typesCount = 1; 211 this.typing = true; 212 213 this.onChange(); 214 } 215 }, 216 0, this ); 217 218 return; 219 } 220 221 this.typesCount++; 222 223 if ( this.typesCount > 25 ) 224 { 225 this.save(); 226 this.typesCount = 1; 227 } 228 229 this.typing = true; 230 }, 231 232 fireChange : function() 233 { 234 this.hasUndo = !!this.getNextImage( true ); 235 this.hasRedo = !!this.getNextImage( false ); 236 237 this.typing = false; 238 this.typesCount = 0; 239 240 this.onChange(); 241 }, 242 243 /** 244 * Save a snapshot of document image for later retrieve. 245 */ 246 save : function( onContentOnly, image ) 247 { 248 var snapshots = this.snapshots; 249 250 // Get a content image. 251 if ( !image ) 252 image = new Image( this.editor ); 253 254 // Check if this is a duplicate. In such case, do nothing. 255 if ( this.currentImage && image.equals( this.currentImage, onContentOnly ) ) 256 return false; 257 258 // Drop future snapshots. 259 snapshots.splice( this.index + 1, snapshots.length - this.index - 1 ); 260 261 // If we have reached the limit, remove the oldest one. 262 if ( snapshots.length == this.limit ) 263 snapshots.shift(); 264 265 // Add the new image, updating the current index. 266 this.index = snapshots.push( image ) - 1; 267 268 this.currentImage = image; 269 270 this.fireChange(); 271 272 return true; 273 }, 274 275 restoreImage : function( image ) 276 { 277 this.editor.loadSnapshot( image.contents ); 278 279 if ( image.bookmarks ) 280 this.editor.getSelection().selectBookmarks( image.bookmarks ); 281 282 this.index = image.index; 283 284 this.currentImage = image; 285 286 this.fireChange(); 287 }, 288 289 // Get the closest available image. 290 getNextImage : function( isUndo ) 291 { 292 var snapshots = this.snapshots, 293 currentImage = this.currentImage, 294 image, i; 295 296 if ( currentImage ) 297 { 298 if ( isUndo ) 299 { 300 for ( i = this.index - 1 ; i >= 0 ; i-- ) 301 { 302 image = snapshots[ i ]; 303 if ( !currentImage.equals( image, true ) ) 304 { 305 image.index = i; 306 return image; 307 } 308 } 309 } 310 else 311 { 312 for ( i = this.index + 1 ; i < snapshots.length ; i++ ) 313 { 314 image = snapshots[ i ]; 315 if ( !currentImage.equals( image, true ) ) 316 { 317 image.index = i; 318 return image; 319 } 320 } 321 } 322 } 323 324 return null; 325 }, 326 327 /** 328 * Check the current redo state. 329 * @return {Boolean} Whether the document has previous state to 330 * retrieve. 331 */ 332 redoable : function() 333 { 334 return this.enabled && this.hasRedo; 335 }, 336 337 /** 338 * Check the current undo state. 339 * @return {Boolean} Whether the document has future state to restore. 340 */ 341 undoable : function() 342 { 343 return this.enabled && this.hasUndo; 344 }, 345 346 /** 347 * Perform undo on current index. 348 */ 349 undo : function() 350 { 351 if ( this.undoable() ) 352 { 353 this.save( true ); 354 355 var image = this.getNextImage( true ); 356 if ( image ) 357 return this.restoreImage( image ), true; 358 } 359 360 return false; 361 }, 362 363 /** 364 * Perform redo on current index. 365 */ 366 redo : function() 367 { 368 if ( this.redoable() ) 369 { 370 // Try to save. If no changes have been made, the redo stack 371 // will not change, so it will still be redoable. 372 this.save( true ); 373 374 // If instead we had changes, we can't redo anymore. 375 if ( this.redoable() ) 376 { 377 var image = this.getNextImage( false ); 378 if ( image ) 379 return this.restoreImage( image ), true; 380 } 381 } 382 383 return false; 384 } 385 }; 386 })(); 387 388 CKEDITOR.config.undoStackSize = 20; 389