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