]> git.immae.eu Git - perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git/blame - sources/plugins/undo/plugin.js
Upgrade to 4.5.7 and add some plugin
[perso/Immae/Projets/packagist/connexionswing-ckeditor-component.git] / sources / plugins / undo / plugin.js
CommitLineData
7adcb81e 1/**\r
3b35bd27 2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.\r
7adcb81e
IB
3 * For licensing, see LICENSE.md or http://ckeditor.com/license\r
4 */\r
5\r
6/**\r
7 * @fileOverview Undo/Redo system for saving a shapshot for document modification\r
8 * and other recordable changes.\r
9 */\r
10\r
11'use strict';\r
12\r
13( function() {\r
14 var keystrokes = [\r
15 CKEDITOR.CTRL + 90 /*Z*/,\r
16 CKEDITOR.CTRL + 89 /*Y*/,\r
17 CKEDITOR.CTRL + CKEDITOR.SHIFT + 90 /*Z*/\r
18 ],\r
19 backspaceOrDelete = { 8: 1, 46: 1 };\r
20\r
21 CKEDITOR.plugins.add( 'undo', {\r
22 // jscs:disable maximumLineLength\r
3b35bd27 23 lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,de-ch,el,en,en-au,en-ca,en-gb,eo,es,et,eu,fa,fi,fo,fr,fr-ca,gl,gu,he,hi,hr,hu,id,is,it,ja,ka,km,ko,ku,lt,lv,mk,mn,ms,nb,nl,no,pl,pt,pt-br,ro,ru,si,sk,sl,sq,sr,sr-latn,sv,th,tr,tt,ug,uk,vi,zh,zh-cn', // %REMOVE_LINE_CORE%\r
7adcb81e
IB
24 // jscs:enable maximumLineLength\r
25 icons: 'redo,redo-rtl,undo,undo-rtl', // %REMOVE_LINE_CORE%\r
26 hidpi: true, // %REMOVE_LINE_CORE%\r
27 init: function( editor ) {\r
28 var undoManager = editor.undoManager = new UndoManager( editor ),\r
29 editingHandler = undoManager.editingHandler = new NativeEditingHandler( undoManager );\r
30\r
31 var undoCommand = editor.addCommand( 'undo', {\r
32 exec: function() {\r
33 if ( undoManager.undo() ) {\r
34 editor.selectionChange();\r
35 this.fire( 'afterUndo' );\r
36 }\r
37 },\r
38 startDisabled: true,\r
39 canUndo: false\r
40 } );\r
41\r
42 var redoCommand = editor.addCommand( 'redo', {\r
43 exec: function() {\r
44 if ( undoManager.redo() ) {\r
45 editor.selectionChange();\r
46 this.fire( 'afterRedo' );\r
47 }\r
48 },\r
49 startDisabled: true,\r
50 canUndo: false\r
51 } );\r
52\r
53 editor.setKeystroke( [\r
54 [ keystrokes[ 0 ], 'undo' ],\r
55 [ keystrokes[ 1 ], 'redo' ],\r
56 [ keystrokes[ 2 ], 'redo' ]\r
57 ] );\r
58\r
59 undoManager.onChange = function() {\r
60 undoCommand.setState( undoManager.undoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );\r
61 redoCommand.setState( undoManager.redoable() ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED );\r
62 };\r
63\r
64 function recordCommand( event ) {\r
65 // If the command hasn't been marked to not support undo.\r
66 if ( undoManager.enabled && event.data.command.canUndo !== false )\r
67 undoManager.save();\r
68 }\r
69\r
70 // We'll save snapshots before and after executing a command.\r
71 editor.on( 'beforeCommandExec', recordCommand );\r
72 editor.on( 'afterCommandExec', recordCommand );\r
73\r
74 // Save snapshots before doing custom changes.\r
75 editor.on( 'saveSnapshot', function( evt ) {\r
76 undoManager.save( evt.data && evt.data.contentOnly );\r
77 } );\r
78\r
79 // Event manager listeners should be attached on contentDom.\r
80 editor.on( 'contentDom', editingHandler.attachListeners, editingHandler );\r
81\r
82 editor.on( 'instanceReady', function() {\r
83 // Saves initial snapshot.\r
84 editor.fire( 'saveSnapshot' );\r
85 } );\r
86\r
87 // Always save an undo snapshot - the previous mode might have\r
88 // changed editor contents.\r
89 editor.on( 'beforeModeUnload', function() {\r
90 editor.mode == 'wysiwyg' && undoManager.save( true );\r
91 } );\r
92\r
93 function toggleUndoManager() {\r
94 undoManager.enabled = editor.readOnly ? false : editor.mode == 'wysiwyg';\r
95 undoManager.onChange();\r
96 }\r
97\r
98 // Make the undo manager available only in wysiwyg mode.\r
99 editor.on( 'mode', toggleUndoManager );\r
100\r
101 // Disable undo manager when in read-only mode.\r
102 editor.on( 'readOnly', toggleUndoManager );\r
103\r
104 if ( editor.ui.addButton ) {\r
105 editor.ui.addButton( 'Undo', {\r
106 label: editor.lang.undo.undo,\r
107 command: 'undo',\r
108 toolbar: 'undo,10'\r
109 } );\r
110\r
111 editor.ui.addButton( 'Redo', {\r
112 label: editor.lang.undo.redo,\r
113 command: 'redo',\r
114 toolbar: 'undo,20'\r
115 } );\r
116 }\r
117\r
118 /**\r
119 * Resets the undo stack.\r
120 *\r
121 * @member CKEDITOR.editor\r
122 */\r
123 editor.resetUndo = function() {\r
124 // Reset the undo stack.\r
125 undoManager.reset();\r
126\r
127 // Create the first image.\r
128 editor.fire( 'saveSnapshot' );\r
129 };\r
130\r
131 /**\r
132 * Amends the top of the undo stack (last undo image) with the current DOM changes.\r
133 *\r
134 * function() {\r
135 * editor.fire( 'saveSnapshot' );\r
136 * editor.document.body.append(...);\r
137 * // Makes new changes following the last undo snapshot a part of it.\r
138 * editor.fire( 'updateSnapshot' );\r
139 * ..\r
140 * }\r
141 *\r
142 * @event updateSnapshot\r
143 * @member CKEDITOR.editor\r
144 * @param {CKEDITOR.editor} editor This editor instance.\r
145 */\r
146 editor.on( 'updateSnapshot', function() {\r
147 if ( undoManager.currentImage )\r
148 undoManager.update();\r
149 } );\r
150\r
151 /**\r
152 * Locks the undo manager to prevent any save/update operations.\r
153 *\r
154 * It is convenient to lock the undo manager before performing DOM operations\r
155 * that should not be recored (e.g. auto paragraphing).\r
156 *\r
157 * See {@link CKEDITOR.plugins.undo.UndoManager#lock} for more details.\r
158 *\r
159 * **Note:** In order to unlock the undo manager, {@link #unlockSnapshot} has to be fired\r
160 * the same number of times that `lockSnapshot` has been fired.\r
161 *\r
162 * @since 4.0\r
163 * @event lockSnapshot\r
164 * @member CKEDITOR.editor\r
165 * @param {CKEDITOR.editor} editor This editor instance.\r
166 * @param data\r
167 * @param {Boolean} [data.dontUpdate] When set to `true`, the last snapshot will not be updated\r
168 * with the current content and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.\r
169 * @param {Boolean} [data.forceUpdate] When set to `true`, the last snapshot will always be updated\r
170 * with the current content and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.\r
171 */\r
172 editor.on( 'lockSnapshot', function( evt ) {\r
173 var data = evt.data;\r
174 undoManager.lock( data && data.dontUpdate, data && data.forceUpdate );\r
175 } );\r
176\r
177 /**\r
178 * Unlocks the undo manager and updates the latest snapshot.\r
179 *\r
180 * @since 4.0\r
181 * @event unlockSnapshot\r
182 * @member CKEDITOR.editor\r
183 * @param {CKEDITOR.editor} editor This editor instance.\r
184 */\r
185 editor.on( 'unlockSnapshot', undoManager.unlock, undoManager );\r
186 }\r
187 } );\r
188\r
189 CKEDITOR.plugins.undo = {};\r
190\r
191 /**\r
192 * Main logic for the Redo/Undo feature.\r
193 *\r
194 * @private\r
195 * @class CKEDITOR.plugins.undo.UndoManager\r
196 * @constructor Creates an UndoManager class instance.\r
197 * @param {CKEDITOR.editor} editor\r
198 */\r
199 var UndoManager = CKEDITOR.plugins.undo.UndoManager = function( editor ) {\r
200 /**\r
201 * An array storing the number of key presses, count in a row. Use {@link #keyGroups} members as index.\r
202 *\r
203 * **Note:** The keystroke count will be reset after reaching the limit of characters per snapshot.\r
204 *\r
205 * @since 4.4.4\r
206 */\r
207 this.strokesRecorded = [ 0, 0 ];\r
208\r
209 /**\r
210 * When the `locked` property is not `null`, the undo manager is locked, so\r
211 * operations like `save` or `update` are forbidden.\r
212 *\r
213 * The manager can be locked and unlocked by the {@link #lock} and {@link #unlock}\r
214 * methods, respectively.\r
215 *\r
216 * @readonly\r
217 * @property {Object} [locked=null]\r
218 */\r
219 this.locked = null;\r
220\r
221 /**\r
222 * Contains the previously processed key group, based on {@link #keyGroups}.\r
223 * `-1` means an unknown group.\r
224 *\r
225 * @since 4.4.4\r
226 * @readonly\r
227 * @property {Number} [previousKeyGroup=-1]\r
228 */\r
229 this.previousKeyGroup = -1;\r
230\r
231 /**\r
232 * The maximum number of snapshots in the stack. Configurable via {@link CKEDITOR.config#undoStackSize}.\r
233 *\r
234 * @readonly\r
235 * @property {Number} [limit]\r
236 */\r
237 this.limit = editor.config.undoStackSize || 20;\r
238\r
239 /**\r
240 * The maximum number of characters typed/deleted in one undo step.\r
241 *\r
242 * @since 4.4.5\r
243 * @readonly\r
244 */\r
245 this.strokesLimit = 25;\r
246\r
247 this.editor = editor;\r
248\r
249 // Reset the undo stack.\r
250 this.reset();\r
251 };\r
252\r
253 UndoManager.prototype = {\r
254 /**\r
255 * Handles keystroke support for the undo manager. It is called on `keyup` event for\r
256 * keystrokes that can change the editor content.\r
257 *\r
258 * @param {Number} keyCode The key code.\r
259 * @param {Boolean} [strokesPerSnapshotExceeded] When set to `true`, the method will\r
260 * behave as if the strokes limit was exceeded regardless of the {@link #strokesRecorded} value.\r
261 */\r
262 type: function( keyCode, strokesPerSnapshotExceeded ) {\r
263 var keyGroup = UndoManager.getKeyGroup( keyCode ),\r
264 // Count of keystrokes in current a row.\r
265 // Note if strokesPerSnapshotExceeded will be exceeded, it'll be restarted.\r
266 strokesRecorded = this.strokesRecorded[ keyGroup ] + 1;\r
267\r
268 strokesPerSnapshotExceeded =\r
269 ( strokesPerSnapshotExceeded || strokesRecorded >= this.strokesLimit );\r
270\r
271 if ( !this.typing )\r
272 onTypingStart( this );\r
273\r
274 if ( strokesPerSnapshotExceeded ) {\r
275 // Reset the count of strokes, so it'll be later assigned to this.strokesRecorded.\r
276 strokesRecorded = 0;\r
277\r
278 this.editor.fire( 'saveSnapshot' );\r
279 } else {\r
280 // Fire change event.\r
281 this.editor.fire( 'change' );\r
282 }\r
283\r
284 // Store recorded strokes count.\r
285 this.strokesRecorded[ keyGroup ] = strokesRecorded;\r
286 // This prop will tell in next itaration what kind of group was processed previously.\r
287 this.previousKeyGroup = keyGroup;\r
288 },\r
289\r
290 /**\r
291 * Whether the new `keyCode` belongs to a different group than the previous one ({@link #previousKeyGroup}).\r
292 *\r
293 * @since 4.4.5\r
294 * @param {Number} keyCode\r
295 * @returns {Boolean}\r
296 */\r
297 keyGroupChanged: function( keyCode ) {\r
298 return UndoManager.getKeyGroup( keyCode ) != this.previousKeyGroup;\r
299 },\r
300\r
301 /**\r
302 * Resets the undo stack.\r
303 */\r
304 reset: function() {\r
305 // Stack for all the undo and redo snapshots, they're always created/removed\r
306 // in consistency.\r
307 this.snapshots = [];\r
308\r
309 // Current snapshot history index.\r
310 this.index = -1;\r
311\r
312 this.currentImage = null;\r
313\r
314 this.hasUndo = false;\r
315 this.hasRedo = false;\r
316 this.locked = null;\r
317\r
318 this.resetType();\r
319 },\r
320\r
321 /**\r
322 * Resets all typing variables.\r
323 *\r
324 * @see #type\r
325 */\r
326 resetType: function() {\r
327 this.strokesRecorded = [ 0, 0 ];\r
328 this.typing = false;\r
329 this.previousKeyGroup = -1;\r
330 },\r
331\r
332 /**\r
333 * Refreshes the state of the {@link CKEDITOR.plugins.undo.UndoManager undo manager}\r
334 * as well as the state of the `undo` and `redo` commands.\r
335 */\r
336 refreshState: function() {\r
337 // These lines can be handled within onChange() too.\r
338 this.hasUndo = !!this.getNextImage( true );\r
339 this.hasRedo = !!this.getNextImage( false );\r
340 // Reset typing\r
341 this.resetType();\r
342 this.onChange();\r
343 },\r
344\r
345 /**\r
346 * Saves a snapshot of the document image for later retrieval.\r
347 *\r
348 * @param {Boolean} onContentOnly If set to `true`, the snapshot will be saved only if the content has changed.\r
349 * @param {CKEDITOR.plugins.undo.Image} image An optional image to save. If skipped, current editor will be used.\r
350 * @param {Boolean} [autoFireChange=true] If set to `false`, will not trigger the {@link CKEDITOR.editor#change} event to editor.\r
351 */\r
352 save: function( onContentOnly, image, autoFireChange ) {\r
353 var editor = this.editor;\r
354 // Do not change snapshots stack when locked, editor is not ready,\r
355 // editable is not ready or when editor is in mode difference than 'wysiwyg'.\r
356 if ( this.locked || editor.status != 'ready' || editor.mode != 'wysiwyg' )\r
357 return false;\r
358\r
359 var editable = editor.editable();\r
360 if ( !editable || editable.status != 'ready' )\r
361 return false;\r
362\r
363 var snapshots = this.snapshots;\r
364\r
365 // Get a content image.\r
366 if ( !image )\r
367 image = new Image( editor );\r
368\r
369 // Do nothing if it was not possible to retrieve an image.\r
370 if ( image.contents === false )\r
371 return false;\r
372\r
373 // Check if this is a duplicate. In such case, do nothing.\r
374 if ( this.currentImage ) {\r
375 if ( image.equalsContent( this.currentImage ) ) {\r
376 if ( onContentOnly )\r
377 return false;\r
378\r
379 if ( image.equalsSelection( this.currentImage ) )\r
380 return false;\r
381 } else if ( autoFireChange !== false ) {\r
382 editor.fire( 'change' );\r
383 }\r
384 }\r
385\r
386 // Drop future snapshots.\r
387 snapshots.splice( this.index + 1, snapshots.length - this.index - 1 );\r
388\r
389 // If we have reached the limit, remove the oldest one.\r
390 if ( snapshots.length == this.limit )\r
391 snapshots.shift();\r
392\r
393 // Add the new image, updating the current index.\r
394 this.index = snapshots.push( image ) - 1;\r
395\r
396 this.currentImage = image;\r
397\r
398 if ( autoFireChange !== false )\r
399 this.refreshState();\r
400 return true;\r
401 },\r
402\r
403 /**\r
404 * Sets editor content/selection to the one stored in `image`.\r
405 *\r
406 * @param {CKEDITOR.plugins.undo.Image} image\r
407 */\r
408 restoreImage: function( image ) {\r
409 // Bring editor focused to restore selection.\r
410 var editor = this.editor,\r
411 sel;\r
412\r
413 if ( image.bookmarks ) {\r
414 editor.focus();\r
415 // Retrieve the selection beforehand. (#8324)\r
416 sel = editor.getSelection();\r
417 }\r
418\r
419 // Start transaction - do not allow any mutations to the\r
420 // snapshots stack done when selecting bookmarks (much probably\r
421 // by selectionChange listener).\r
422 this.locked = { level: 999 };\r
423\r
424 this.editor.loadSnapshot( image.contents );\r
425\r
426 if ( image.bookmarks )\r
427 sel.selectBookmarks( image.bookmarks );\r
428 else if ( CKEDITOR.env.ie ) {\r
429 // IE BUG: If I don't set the selection to *somewhere* after setting\r
430 // document contents, then IE would create an empty paragraph at the bottom\r
431 // the next time the document is modified.\r
432 var $range = this.editor.document.getBody().$.createTextRange();\r
433 $range.collapse( true );\r
434 $range.select();\r
435 }\r
436\r
437 this.locked = null;\r
438\r
439 this.index = image.index;\r
440 this.currentImage = this.snapshots[ this.index ];\r
441\r
442 // Update current image with the actual editor\r
443 // content, since actualy content may differ from\r
444 // the original snapshot due to dom change. (#4622)\r
445 this.update();\r
446 this.refreshState();\r
447\r
448 editor.fire( 'change' );\r
449 },\r
450\r
451 /**\r
452 * Gets the closest available image.\r
453 *\r
454 * @param {Boolean} isUndo If `true`, it will return the previous image.\r
455 * @returns {CKEDITOR.plugins.undo.Image} Next image or `null`.\r
456 */\r
457 getNextImage: function( isUndo ) {\r
458 var snapshots = this.snapshots,\r
459 currentImage = this.currentImage,\r
460 image, i;\r
461\r
462 if ( currentImage ) {\r
463 if ( isUndo ) {\r
464 for ( i = this.index - 1; i >= 0; i-- ) {\r
465 image = snapshots[ i ];\r
466 if ( !currentImage.equalsContent( image ) ) {\r
467 image.index = i;\r
468 return image;\r
469 }\r
470 }\r
471 } else {\r
472 for ( i = this.index + 1; i < snapshots.length; i++ ) {\r
473 image = snapshots[ i ];\r
474 if ( !currentImage.equalsContent( image ) ) {\r
475 image.index = i;\r
476 return image;\r
477 }\r
478 }\r
479 }\r
480 }\r
481\r
482 return null;\r
483 },\r
484\r
485 /**\r
486 * Checks the current redo state.\r
487 *\r
488 * @returns {Boolean} Whether the document has a previous state to retrieve.\r
489 */\r
490 redoable: function() {\r
491 return this.enabled && this.hasRedo;\r
492 },\r
493\r
494 /**\r
495 * Checks the current undo state.\r
496 *\r
497 * @returns {Boolean} Whether the document has a future state to restore.\r
498 */\r
499 undoable: function() {\r
500 return this.enabled && this.hasUndo;\r
501 },\r
502\r
503 /**\r
504 * Performs an undo operation on current index.\r
505 */\r
506 undo: function() {\r
507 if ( this.undoable() ) {\r
508 this.save( true );\r
509\r
510 var image = this.getNextImage( true );\r
511 if ( image )\r
512 return this.restoreImage( image ), true;\r
513 }\r
514\r
515 return false;\r
516 },\r
517\r
518 /**\r
519 * Performs a redo operation on current index.\r
520 */\r
521 redo: function() {\r
522 if ( this.redoable() ) {\r
523 // Try to save. If no changes have been made, the redo stack\r
524 // will not change, so it will still be redoable.\r
525 this.save( true );\r
526\r
527 // If instead we had changes, we can't redo anymore.\r
528 if ( this.redoable() ) {\r
529 var image = this.getNextImage( false );\r
530 if ( image )\r
531 return this.restoreImage( image ), true;\r
532 }\r
533 }\r
534\r
535 return false;\r
536 },\r
537\r
538 /**\r
539 * Updates the last snapshot of the undo stack with the current editor content.\r
540 *\r
541 * @param {CKEDITOR.plugins.undo.Image} [newImage] The image which will replace the current one.\r
542 * If it is not set, it defaults to the image taken from the editor.\r
543 */\r
544 update: function( newImage ) {\r
545 // Do not change snapshots stack is locked.\r
546 if ( this.locked )\r
547 return;\r
548\r
549 if ( !newImage )\r
550 newImage = new Image( this.editor );\r
551\r
552 var i = this.index,\r
553 snapshots = this.snapshots;\r
554\r
555 // Find all previous snapshots made for the same content (which differ\r
556 // only by selection) and replace all of them with the current image.\r
557 while ( i > 0 && this.currentImage.equalsContent( snapshots[ i - 1 ] ) )\r
558 i -= 1;\r
559\r
560 snapshots.splice( i, this.index - i + 1, newImage );\r
561 this.index = i;\r
562 this.currentImage = newImage;\r
563 },\r
564\r
565 /**\r
566 * Amends the last snapshot and changes its selection (only in case when content\r
567 * is equal between these two).\r
568 *\r
569 * @since 4.4.4\r
570 * @param {CKEDITOR.plugins.undo.Image} newSnapshot New snapshot with new selection.\r
571 * @returns {Boolean} Returns `true` if selection was amended.\r
572 */\r
573 updateSelection: function( newSnapshot ) {\r
574 if ( !this.snapshots.length )\r
575 return false;\r
576\r
577 var snapshots = this.snapshots,\r
578 lastImage = snapshots[ snapshots.length - 1 ];\r
579\r
580 if ( lastImage.equalsContent( newSnapshot ) ) {\r
581 if ( !lastImage.equalsSelection( newSnapshot ) ) {\r
582 snapshots[ snapshots.length - 1 ] = newSnapshot;\r
583 this.currentImage = newSnapshot;\r
584 return true;\r
585 }\r
586 }\r
587\r
588 return false;\r
589 },\r
590\r
591 /**\r
592 * Locks the snapshot stack to prevent any save/update operations and when necessary,\r
593 * updates the tip of the snapshot stack with the DOM changes introduced during the\r
594 * locked period, after the {@link #unlock} method is called.\r
595 *\r
596 * It is mainly used to ensure any DOM operations that should not be recorded\r
597 * (e.g. auto paragraphing) are not added to the stack.\r
598 *\r
599 * **Note:** For every `lock` call you must call {@link #unlock} once to unlock the undo manager.\r
600 *\r
601 * @since 4.0\r
602 * @param {Boolean} [dontUpdate] When set to `true`, the last snapshot will not be updated\r
603 * with current content and selection. By default, if undo manager was up to date when the lock started,\r
604 * the last snapshot will be updated to the current state when unlocking. This means that all changes\r
605 * done during the lock will be merged into the previous snapshot or the next one. Use this option to gain\r
606 * more control over this behavior. For example, it is possible to group changes done during the lock into\r
607 * a separate snapshot.\r
608 * @param {Boolean} [forceUpdate] When set to `true`, the last snapshot will always be updated with the\r
609 * current content and selection regardless of the current state of the undo manager.\r
610 * When not set, the last snapshot will be updated only if the undo manager was up to date when locking.\r
611 * Additionally, this option makes it possible to lock the snapshot when the editor is not in the `wysiwyg` mode,\r
612 * because when it is passed, the snapshots will not need to be compared.\r
613 */\r
614 lock: function( dontUpdate, forceUpdate ) {\r
615 if ( !this.locked ) {\r
616 if ( dontUpdate )\r
617 this.locked = { level: 1 };\r
618 else {\r
619 var update = null;\r
620\r
621 if ( forceUpdate )\r
622 update = true;\r
623 else {\r
624 // Make a contents image. Don't include bookmarks, because:\r
625 // * we don't compare them,\r
626 // * there's a chance that DOM has been changed since\r
627 // locked (e.g. fake) selection was made, so createBookmark2 could fail.\r
628 // http://dev.ckeditor.com/ticket/11027#comment:3\r
629 var imageBefore = new Image( this.editor, true );\r
630\r
631 // If current editor content matches the tip of snapshot stack,\r
632 // the stack tip must be updated by unlock, to include any changes made\r
633 // during this period.\r
634 if ( this.currentImage && this.currentImage.equalsContent( imageBefore ) )\r
635 update = imageBefore;\r
636 }\r
637\r
638 this.locked = { update: update, level: 1 };\r
639 }\r
640\r
641 // Increase the level of lock.\r
642 } else {\r
643 this.locked.level++;\r
644 }\r
645 },\r
646\r
647 /**\r
648 * Unlocks the snapshot stack and checks to amend the last snapshot.\r
649 *\r
650 * See {@link #lock} for more details.\r
651 *\r
652 * @since 4.0\r
653 */\r
654 unlock: function() {\r
655 if ( this.locked ) {\r
656 // Decrease level of lock and check if equals 0, what means that undoM is completely unlocked.\r
657 if ( !--this.locked.level ) {\r
658 var update = this.locked.update;\r
659\r
660 this.locked = null;\r
661\r
662 // forceUpdate was passed to lock().\r
663 if ( update === true )\r
664 this.update();\r
665 // update is instance of Image.\r
666 else if ( update ) {\r
667 var newImage = new Image( this.editor, true );\r
668\r
669 if ( !update.equalsContent( newImage ) )\r
670 this.update();\r
671 }\r
672 }\r
673 }\r
674 }\r
675 };\r
676\r
677 /**\r
678 * Codes for navigation keys like *Arrows*, *Page Up/Down*, etc.\r
679 * Used by the {@link #isNavigationKey} method.\r
680 *\r
681 * @since 4.4.5\r
682 * @readonly\r
683 * @static\r
684 */\r
685 UndoManager.navigationKeyCodes = {\r
686 37: 1, 38: 1, 39: 1, 40: 1, // Arrows.\r
687 36: 1, 35: 1, // Home, End.\r
688 33: 1, 34: 1 // PgUp, PgDn.\r
689 };\r
690\r
691 /**\r
692 * Key groups identifier mapping. Used for accessing members in\r
693 * {@link #strokesRecorded}.\r
694 *\r
695 * * `FUNCTIONAL` &ndash; identifier for the *Backspace* / *Delete* key.\r
696 * * `PRINTABLE` &ndash; identifier for printable keys.\r
697 *\r
698 * Example usage:\r
699 *\r
700 * undoManager.strokesRecorded[ undoManager.keyGroups.FUNCTIONAL ];\r
701 *\r
702 * @since 4.4.5\r
703 * @readonly\r
704 * @static\r
705 */\r
706 UndoManager.keyGroups = {\r
707 PRINTABLE: 0,\r
708 FUNCTIONAL: 1\r
709 };\r
710\r
711 /**\r
712 * Checks whether a key is one of navigation keys (*Arrows*, *Page Up/Down*, etc.).\r
713 * See also the {@link #navigationKeyCodes} property.\r
714 *\r
715 * @since 4.4.5\r
716 * @static\r
717 * @param {Number} keyCode\r
718 * @returns {Boolean}\r
719 */\r
720 UndoManager.isNavigationKey = function( keyCode ) {\r
721 return !!UndoManager.navigationKeyCodes[ keyCode ];\r
722 };\r
723\r
724 /**\r
725 * Returns the group to which the passed `keyCode` belongs.\r
726 *\r
727 * @since 4.4.5\r
728 * @static\r
729 * @param {Number} keyCode\r
730 * @returns {Number}\r
731 */\r
732 UndoManager.getKeyGroup = function( keyCode ) {\r
733 var keyGroups = UndoManager.keyGroups;\r
734\r
735 return backspaceOrDelete[ keyCode ] ? keyGroups.FUNCTIONAL : keyGroups.PRINTABLE;\r
736 };\r
737\r
738 /**\r
739 * @since 4.4.5\r
740 * @static\r
741 * @param {Number} keyGroup\r
742 * @returns {Number}\r
743 */\r
744 UndoManager.getOppositeKeyGroup = function( keyGroup ) {\r
745 var keyGroups = UndoManager.keyGroups;\r
746 return ( keyGroup == keyGroups.FUNCTIONAL ? keyGroups.PRINTABLE : keyGroups.FUNCTIONAL );\r
747 };\r
748\r
749 /**\r
750 * Whether we need to use a workaround for functional (*Backspace*, *Delete*) keys not firing\r
751 * the `keypress` event in Internet Explorer in this environment and for the specified `keyCode`.\r
752 *\r
753 * @since 4.4.5\r
754 * @static\r
755 * @param {Number} keyCode\r
756 * @returns {Boolean}\r
757 */\r
758 UndoManager.ieFunctionalKeysBug = function( keyCode ) {\r
759 return CKEDITOR.env.ie && UndoManager.getKeyGroup( keyCode ) == UndoManager.keyGroups.FUNCTIONAL;\r
760 };\r
761\r
762 // Helper method called when undoManager.typing val was changed to true.\r
763 function onTypingStart( undoManager ) {\r
764 // It's safe to now indicate typing state.\r
765 undoManager.typing = true;\r
766\r
767 // Manually mark snapshot as available.\r
768 undoManager.hasUndo = true;\r
769 undoManager.hasRedo = false;\r
770\r
771 undoManager.onChange();\r
772 }\r
773\r
774 /**\r
775 * Contains a snapshot of the editor content and selection at a given point in time.\r
776 *\r
777 * @private\r
778 * @class CKEDITOR.plugins.undo.Image\r
779 * @constructor Creates an Image class instance.\r
780 * @param {CKEDITOR.editor} editor The editor instance on which the image is created.\r
781 * @param {Boolean} [contentsOnly] If set to `true`, the image will only contain content without the selection.\r
782 */\r
783 var Image = CKEDITOR.plugins.undo.Image = function( editor, contentsOnly ) {\r
784 this.editor = editor;\r
785\r
786 editor.fire( 'beforeUndoImage' );\r
787\r
788 var contents = editor.getSnapshot();\r
789\r
790 // In IE, we need to remove the expando attributes.\r
791 if ( CKEDITOR.env.ie && contents )\r
792 contents = contents.replace( /\s+data-cke-expando=".*?"/g, '' );\r
793\r
794 this.contents = contents;\r
795\r
796 if ( !contentsOnly ) {\r
797 var selection = contents && editor.getSelection();\r
798 this.bookmarks = selection && selection.createBookmarks2( true );\r
799 }\r
800\r
801 editor.fire( 'afterUndoImage' );\r
802 };\r
803\r
804 // Attributes that browser may changing them when setting via innerHTML.\r
805 var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi;\r
806\r
807 Image.prototype = {\r
808 /**\r
809 * @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to.\r
810 * @returns {Boolean} Returns `true` if content in `otherImage` is the same.\r
811 */\r
812 equalsContent: function( otherImage ) {\r
813 var thisContents = this.contents,\r
814 otherContents = otherImage.contents;\r
815\r
816 // For IE7 and IE QM: Comparing only the protected attribute values but not the original ones.(#4522)\r
817 if ( CKEDITOR.env.ie && ( CKEDITOR.env.ie7Compat || CKEDITOR.env.quirks ) ) {\r
818 thisContents = thisContents.replace( protectedAttrs, '' );\r
819 otherContents = otherContents.replace( protectedAttrs, '' );\r
820 }\r
821\r
822 if ( thisContents != otherContents )\r
823 return false;\r
824\r
825 return true;\r
826 },\r
827\r
828 /**\r
829 * @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to.\r
830 * @returns {Boolean} Returns `true` if selection in `otherImage` is the same.\r
831 */\r
832 equalsSelection: function( otherImage ) {\r
833 var bookmarksA = this.bookmarks,\r
834 bookmarksB = otherImage.bookmarks;\r
835\r
836 if ( bookmarksA || bookmarksB ) {\r
837 if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length )\r
838 return false;\r
839\r
840 for ( var i = 0; i < bookmarksA.length; i++ ) {\r
841 var bookmarkA = bookmarksA[ i ],\r
842 bookmarkB = bookmarksB[ i ];\r
843\r
844 if ( bookmarkA.startOffset != bookmarkB.startOffset || bookmarkA.endOffset != bookmarkB.endOffset ||\r
845 !CKEDITOR.tools.arrayCompare( bookmarkA.start, bookmarkB.start ) ||\r
846 !CKEDITOR.tools.arrayCompare( bookmarkA.end, bookmarkB.end ) ) {\r
847 return false;\r
848 }\r
849 }\r
850 }\r
851\r
852 return true;\r
853 }\r
854\r
855 /**\r
856 * Editor content.\r
857 *\r
858 * @readonly\r
859 * @property {String} contents\r
860 */\r
861\r
862 /**\r
863 * Bookmarks representing the selection in an image.\r
864 *\r
865 * @readonly\r
866 * @property {Object[]} bookmarks Array of bookmark2 objects, see {@link CKEDITOR.dom.range#createBookmark2} for definition.\r
867 */\r
868 };\r
869\r
870 /**\r
871 * A class encapsulating all native event listeners which have to be used in\r
872 * order to handle undo manager integration for native editing actions (excluding drag and drop and paste support\r
873 * handled by the Clipboard plugin).\r
874 *\r
875 * @since 4.4.4\r
876 * @private\r
877 * @class CKEDITOR.plugins.undo.NativeEditingHandler\r
878 * @member CKEDITOR.plugins.undo Undo manager owning the handler.\r
879 * @constructor\r
880 * @param {CKEDITOR.plugins.undo.UndoManager} undoManager\r
881 */\r
882 var NativeEditingHandler = CKEDITOR.plugins.undo.NativeEditingHandler = function( undoManager ) {\r
883 // We'll use keyboard + input events to determine if snapshot should be created.\r
884 // Since `input` event is fired before `keyup`. We can tell in `keyup` event if input occured.\r
885 // That will tell us if any printable data was inserted.\r
886 // On `input` event we'll increase input fired counter for proper key code.\r
887 // Eventually it might be canceled by paste/drop using `ignoreInputEvent` flag.\r
888 // Order of events can be found in http://www.w3.org/TR/DOM-Level-3-Events/\r
889\r
890 /**\r
891 * An undo manager instance owning the editing handler.\r
892 *\r
893 * @property {CKEDITOR.plugins.undo.UndoManager} undoManager\r
894 */\r
895 this.undoManager = undoManager;\r
896\r
897 /**\r
898 * See {@link #ignoreInputEventListener}.\r
899 *\r
900 * @since 4.4.5\r
901 * @private\r
902 */\r
903 this.ignoreInputEvent = false;\r
904\r
905 /**\r
906 * A stack of pressed keys.\r
907 *\r
908 * @since 4.4.5\r
909 * @property {CKEDITOR.plugins.undo.KeyEventsStack} keyEventsStack\r
910 */\r
911 this.keyEventsStack = new KeyEventsStack();\r
912\r
913 /**\r
914 * An image of the editor during the `keydown` event (therefore without DOM modification).\r
915 *\r
916 * @property {CKEDITOR.plugins.undo.Image} lastKeydownImage\r
917 */\r
918 this.lastKeydownImage = null;\r
919 };\r
920\r
921 NativeEditingHandler.prototype = {\r
922 /**\r
923 * The `keydown` event listener.\r
924 *\r
925 * @param {CKEDITOR.dom.event} evt\r
926 */\r
927 onKeydown: function( evt ) {\r
928 var keyCode = evt.data.getKey();\r
929\r
930 // The composition is in progress - ignore the key. (#12597)\r
931 if ( keyCode === 229 ) {\r
932 return;\r
933 }\r
934\r
935 // Block undo/redo keystrokes when at the bottom/top of the undo stack (#11126 and #11677).\r
936 if ( CKEDITOR.tools.indexOf( keystrokes, evt.data.getKeystroke() ) > -1 ) {\r
937 evt.data.preventDefault();\r
938 return;\r
939 }\r
940\r
941 // Cleaning tab functional keys.\r
942 this.keyEventsStack.cleanUp( evt );\r
943\r
944 var undoManager = this.undoManager;\r
945\r
946 // Gets last record for provided keyCode. If not found will create one.\r
947 var last = this.keyEventsStack.getLast( keyCode );\r
948 if ( !last ) {\r
949 this.keyEventsStack.push( keyCode );\r
950 }\r
951\r
952 // We need to store an image which will be used in case of key group\r
953 // change.\r
954 this.lastKeydownImage = new Image( undoManager.editor );\r
955\r
956 if ( UndoManager.isNavigationKey( keyCode ) || this.undoManager.keyGroupChanged( keyCode ) ) {\r
957 if ( undoManager.strokesRecorded[ 0 ] || undoManager.strokesRecorded[ 1 ] ) {\r
958 // We already have image, so we'd like to reuse it.\r
959\r
960 // #12300\r
961 undoManager.save( false, this.lastKeydownImage, false );\r
962 undoManager.resetType();\r
963 }\r
964 }\r
965 },\r
966\r
967 /**\r
968 * The `input` event listener.\r
969 */\r
970 onInput: function() {\r
971 // Input event is ignored if paste/drop event were fired before.\r
972 if ( this.ignoreInputEvent ) {\r
973 // Reset flag - ignore only once.\r
974 this.ignoreInputEvent = false;\r
975 return;\r
976 }\r
977\r
978 var lastInput = this.keyEventsStack.getLast();\r
979 // Nothing in key events stack, but input event called. Interesting...\r
980 // That's because on Android order of events is buggy and also keyCode is set to 0.\r
981 if ( !lastInput ) {\r
982 lastInput = this.keyEventsStack.push( 0 );\r
983 }\r
984\r
985 // Increment inputs counter for provided key code.\r
986 this.keyEventsStack.increment( lastInput.keyCode );\r
987\r
988 // Exceeded limit.\r
989 if ( this.keyEventsStack.getTotalInputs() >= this.undoManager.strokesLimit ) {\r
990 this.undoManager.type( lastInput.keyCode, true );\r
991 this.keyEventsStack.resetInputs();\r
992 }\r
993 },\r
994\r
995 /**\r
996 * The `keyup` event listener.\r
997 *\r
998 * @param {CKEDITOR.dom.event} evt\r
999 */\r
1000 onKeyup: function( evt ) {\r
1001 var undoManager = this.undoManager,\r
1002 keyCode = evt.data.getKey(),\r
1003 totalInputs = this.keyEventsStack.getTotalInputs();\r
1004\r
1005 // Remove record from stack for provided key code.\r
1006 this.keyEventsStack.remove( keyCode );\r
1007\r
1008 // Second part of the workaround for IEs functional keys bug. We need to check whether something has really\r
1009 // changed because we blindly mocked the keypress event.\r
1010 // Also we need to be aware that lastKeydownImage might not be available (#12327).\r
1011 if ( UndoManager.ieFunctionalKeysBug( keyCode ) && this.lastKeydownImage &&\r
1012 this.lastKeydownImage.equalsContent( new Image( undoManager.editor, true ) ) ) {\r
1013 return;\r
1014 }\r
1015\r
1016 if ( totalInputs > 0 ) {\r
1017 undoManager.type( keyCode );\r
1018 } else if ( UndoManager.isNavigationKey( keyCode ) ) {\r
1019 // Note content snapshot has been checked in keydown.\r
1020 this.onNavigationKey( true );\r
1021 }\r
1022 },\r
1023\r
1024 /**\r
1025 * Method called for navigation change. At first it will check if current content does not differ\r
1026 * from the last saved snapshot.\r
1027 *\r
1028 * * If the content is different, the method creates a standard, extra snapshot.\r
1029 * * If the content is not different, the method will compare the selection, and will\r
1030 * amend the last snapshot selection if it changed.\r
1031 *\r
1032 * @param {Boolean} skipContentCompare If set to `true`, it will not compare content, and only do a selection check.\r
1033 */\r
1034 onNavigationKey: function( skipContentCompare ) {\r
1035 var undoManager = this.undoManager;\r
1036\r
1037 // We attempt to save content snapshot, if content didn't change, we'll\r
1038 // only amend selection.\r
1039 if ( skipContentCompare || !undoManager.save( true, null, false ) )\r
1040 undoManager.updateSelection( new Image( undoManager.editor ) );\r
1041\r
1042 undoManager.resetType();\r
1043 },\r
1044\r
1045 /**\r
1046 * Makes the next `input` event to be ignored.\r
1047 */\r
1048 ignoreInputEventListener: function() {\r
1049 this.ignoreInputEvent = true;\r
1050 },\r
1051\r
1052 /**\r
1053 * Attaches editable listeners required to provide the undo functionality.\r
1054 */\r
1055 attachListeners: function() {\r
1056 var editor = this.undoManager.editor,\r
1057 editable = editor.editable(),\r
1058 that = this;\r
1059\r
1060 // We'll create a snapshot here (before DOM modification), because we'll\r
1061 // need unmodified content when we got keygroup toggled in keyup.\r
1062 editable.attachListener( editable, 'keydown', function( evt ) {\r
1063 that.onKeydown( evt );\r
1064\r
1065 // On IE keypress isn't fired for functional (backspace/delete) keys.\r
1066 // Let's pretend that something's changed.\r
1067 if ( UndoManager.ieFunctionalKeysBug( evt.data.getKey() ) ) {\r
1068 that.onInput();\r
1069 }\r
1070 }, null, null, 999 );\r
1071\r
1072 // Only IE can't use input event, because it's not fired in contenteditable.\r
1073 editable.attachListener( editable, ( CKEDITOR.env.ie ? 'keypress' : 'input' ), that.onInput, that, null, 999 );\r
1074\r
1075 // Keyup executes main snapshot logic.\r
1076 editable.attachListener( editable, 'keyup', that.onKeyup, that, null, 999 );\r
1077\r
1078 // On paste and drop we need to ignore input event.\r
1079 // It would result with calling undoManager.type() on any following key.\r
1080 editable.attachListener( editable, 'paste', that.ignoreInputEventListener, that, null, 999 );\r
1081 editable.attachListener( editable, 'drop', that.ignoreInputEventListener, that, null, 999 );\r
1082\r
1083 // Click should create a snapshot if needed, but shouldn't cause change event.\r
1084 // Don't pass onNavigationKey directly as a listener because it accepts one argument which\r
1085 // will conflict with evt passed to listener.\r
1086 // #12324 comment:4\r
1087 editable.attachListener( editable.isInline() ? editable : editor.document.getDocumentElement(), 'click', function() {\r
1088 that.onNavigationKey();\r
1089 }, null, null, 999 );\r
1090\r
1091 // When pressing `Tab` key while editable is focused, `keyup` event is not fired.\r
1092 // Which means that record for `tab` key stays in key events stack.\r
1093 // We assume that when editor is blurred `tab` key is already up.\r
1094 editable.attachListener( this.undoManager.editor, 'blur', function() {\r
1095 that.keyEventsStack.remove( 9 /*Tab*/ );\r
1096 }, null, null, 999 );\r
1097 }\r
1098 };\r
1099\r
1100 /**\r
1101 * This class represents a stack of pressed keys and stores information\r
1102 * about how many `input` events each key press has caused.\r
1103 *\r
1104 * @since 4.4.5\r
1105 * @private\r
1106 * @class CKEDITOR.plugins.undo.KeyEventsStack\r
1107 * @constructor\r
1108 */\r
1109 var KeyEventsStack = CKEDITOR.plugins.undo.KeyEventsStack = function() {\r
1110 /**\r
1111 * @readonly\r
1112 */\r
1113 this.stack = [];\r
1114 };\r
1115\r
1116 KeyEventsStack.prototype = {\r
1117 /**\r
1118 * Pushes a literal object with two keys: `keyCode` and `inputs` (whose initial value is set to `0`) to stack.\r
1119 * It is intended to be called on the `keydown` event.\r
1120 *\r
1121 * @param {Number} keyCode\r
1122 */\r
1123 push: function( keyCode ) {\r
1124 var length = this.stack.push( { keyCode: keyCode, inputs: 0 } );\r
1125 return this.stack[ length - 1 ];\r
1126 },\r
1127\r
1128 /**\r
1129 * Returns the index of the last registered `keyCode` in the stack.\r
1130 * If no `keyCode` is provided, then the function will return the index of the last item.\r
1131 * If an item is not found, it will return `-1`.\r
1132 *\r
1133 * @param {Number} [keyCode]\r
1134 * @returns {Number}\r
1135 */\r
1136 getLastIndex: function( keyCode ) {\r
1137 if ( typeof keyCode != 'number' ) {\r
1138 return this.stack.length - 1; // Last index or -1.\r
1139 } else {\r
1140 var i = this.stack.length;\r
1141 while ( i-- ) {\r
1142 if ( this.stack[ i ].keyCode == keyCode ) {\r
1143 return i;\r
1144 }\r
1145 }\r
1146 return -1;\r
1147 }\r
1148 },\r
1149\r
1150 /**\r
1151 * Returns the last key recorded in the stack. If `keyCode` is provided, then it will return\r
1152 * the last record for this `keyCode`.\r
1153 *\r
1154 * @param {Number} [keyCode]\r
1155 * @returns {Object} Last matching record or `null`.\r
1156 */\r
1157 getLast: function( keyCode ) {\r
1158 var index = this.getLastIndex( keyCode );\r
1159 if ( index != -1 ) {\r
1160 return this.stack[ index ];\r
1161 } else {\r
1162 return null;\r
1163 }\r
1164 },\r
1165\r
1166 /**\r
1167 * Increments registered input events for stack record for a given `keyCode`.\r
1168 *\r
1169 * @param {Number} keyCode\r
1170 */\r
1171 increment: function( keyCode ) {\r
1172 var found = this.getLast( keyCode );\r
1173 if ( !found ) { // %REMOVE_LINE%\r
1174 throw new Error( 'Trying to increment, but could not found by keyCode: ' + keyCode + '.' ); // %REMOVE_LINE%\r
1175 } // %REMOVE_LINE%\r
1176\r
1177 found.inputs++;\r
1178 },\r
1179\r
1180 /**\r
1181 * Removes the last record from the stack for the provided `keyCode`.\r
1182 *\r
1183 * @param {Number} keyCode\r
1184 */\r
1185 remove: function( keyCode ) {\r
1186 var index = this.getLastIndex( keyCode );\r
1187\r
1188 if ( index != -1 ) {\r
1189 this.stack.splice( index, 1 );\r
1190 }\r
1191 },\r
1192\r
1193 /**\r
1194 * Resets the `inputs` value to `0` for a given `keyCode` or in entire stack if a\r
1195 * `keyCode` is not specified.\r
1196 *\r
1197 * @param {Number} [keyCode]\r
1198 */\r
1199 resetInputs: function( keyCode ) {\r
1200 if ( typeof keyCode == 'number' ) {\r
1201 var last = this.getLast( keyCode );\r
1202\r
1203 if ( !last ) { // %REMOVE_LINE%\r
1204 throw new Error( 'Trying to reset inputs count, but could not found by keyCode: ' + keyCode + '.' ); // %REMOVE_LINE%\r
1205 } // %REMOVE_LINE%\r
1206\r
1207 last.inputs = 0;\r
1208 } else {\r
1209 var i = this.stack.length;\r
1210 while ( i-- ) {\r
1211 this.stack[ i ].inputs = 0;\r
1212 }\r
1213 }\r
1214 },\r
1215\r
1216 /**\r
1217 * Sums up inputs number for each key code and returns it.\r
1218 *\r
1219 * @returns {Number}\r
1220 */\r
1221 getTotalInputs: function() {\r
1222 var i = this.stack.length,\r
1223 total = 0;\r
1224\r
1225 while ( i-- ) {\r
1226 total += this.stack[ i ].inputs;\r
1227 }\r
1228 return total;\r
1229 },\r
1230\r
1231 /**\r
1232 * Cleans the stack based on a provided `keydown` event object. The rationale behind this method\r
1233 * is that some keystrokes cause the `keydown` event to be fired in the editor, but not the `keyup` event.\r
1234 * For instance, *Alt+Tab* will fire `keydown`, but since the editor is blurred by it, then there is\r
1235 * no `keyup`, so the keystroke is not removed from the stack.\r
1236 *\r
1237 * @param {CKEDITOR.dom.event} event\r
1238 */\r
1239 cleanUp: function( event ) {\r
1240 var nativeEvent = event.data.$;\r
1241\r
1242 if ( !( nativeEvent.ctrlKey || nativeEvent.metaKey ) ) {\r
1243 this.remove( 17 );\r
1244 }\r
1245 if ( !nativeEvent.shiftKey ) {\r
1246 this.remove( 16 );\r
1247 }\r
1248 if ( !nativeEvent.altKey ) {\r
1249 this.remove( 18 );\r
1250 }\r
1251 }\r
1252 };\r
1253} )();\r
1254\r
1255/**\r
1256 * The number of undo steps to be saved. The higher value is set, the more\r
1257 * memory is used for it.\r
1258 *\r
1259 * config.undoStackSize = 50;\r
1260 *\r
1261 * @cfg {Number} [undoStackSize=20]\r
1262 * @member CKEDITOR.config\r
1263 */\r
1264\r
1265/**\r
1266 * Fired when the editor is about to save an undo snapshot. This event can be\r
1267 * fired by plugins and customizations to make the editor save undo snapshots.\r
1268 *\r
1269 * @event saveSnapshot\r
1270 * @member CKEDITOR.editor\r
1271 * @param {CKEDITOR.editor} editor This editor instance.\r
1272 */\r
1273\r
1274/**\r
1275 * Fired before an undo image is to be created. An *undo image* represents the\r
1276 * editor state at some point. It is saved into the undo store, so the editor is\r
1277 * able to recover the editor state on undo and redo operations.\r
1278 *\r
1279 * @since 3.5.3\r
1280 * @event beforeUndoImage\r
1281 * @member CKEDITOR.editor\r
1282 * @param {CKEDITOR.editor} editor This editor instance.\r
1283 * @see CKEDITOR.editor#afterUndoImage\r
1284 */\r
1285\r
1286/**\r
1287 * Fired after an undo image is created. An *undo image* represents the\r
1288 * editor state at some point. It is saved into the undo store, so the editor is\r
1289 * able to recover the editor state on undo and redo operations.\r
1290 *\r
1291 * @since 3.5.3\r
1292 * @event afterUndoImage\r
1293 * @member CKEDITOR.editor\r
1294 * @param {CKEDITOR.editor} editor This editor instance.\r
1295 * @see CKEDITOR.editor#beforeUndoImage\r
1296 */\r
1297\r
1298/**\r
1299 * Fired when the content of the editor is changed.\r
1300 *\r
1301 * Due to performance reasons, it is not verified if the content really changed.\r
1302 * The editor instead watches several editing actions that usually result in\r
1303 * changes. This event may thus in some cases be fired when no changes happen\r
1304 * or may even get fired twice.\r
1305 *\r
1306 * If it is important not to get the `change` event fired too often, you should compare the\r
1307 * previous and the current editor content inside the event listener. It is\r
1308 * not recommended to do that on every `change` event.\r
1309 *\r
1310 * Please note that the `change` event is only fired in the {@link #property-mode wysiwyg mode}.\r
1311 * In order to implement similar functionality in the source mode, you can listen for example to the {@link #key}\r
1312 * event or the native [`input`](https://developer.mozilla.org/en-US/docs/Web/Reference/Events/input)\r
1313 * event (not supported by Internet Explorer 8).\r
1314 *\r
1315 * editor.on( 'mode', function() {\r
1316 * if ( this.mode == 'source' ) {\r
1317 * var editable = editor.editable();\r
1318 * editable.attachListener( editable, 'input', function() {\r
1319 * // Handle changes made in the source mode.\r
1320 * } );\r
1321 * }\r
1322 * } );\r
1323 *\r
1324 * @since 4.2\r
1325 * @event change\r
1326 * @member CKEDITOR.editor\r
1327 * @param {CKEDITOR.editor} editor This editor instance.\r
1328 */\r