2 * @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
3 * For licensing, see LICENSE.md or http://ckeditor.com/license
7 * @fileOverview Undo/Redo system for saving a shapshot for document modification
8 * and other recordable changes.
15 CKEDITOR
.CTRL
+ 90 /*Z*/,
16 CKEDITOR
.CTRL
+ 89 /*Y*/,
17 CKEDITOR
.CTRL
+ CKEDITOR
.SHIFT
+ 90 /*Z*/
19 backspaceOrDelete
= { 8: 1, 46: 1 };
21 CKEDITOR
.plugins
.add( 'undo', {
22 // jscs:disable maximumLineLength
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%
24 // jscs:enable maximumLineLength
25 icons: 'redo,redo-rtl,undo,undo-rtl', // %REMOVE_LINE_CORE%
26 hidpi: true, // %REMOVE_LINE_CORE%
27 init: function( editor
) {
28 var undoManager
= editor
.undoManager
= new UndoManager( editor
),
29 editingHandler
= undoManager
.editingHandler
= new NativeEditingHandler( undoManager
);
31 var undoCommand
= editor
.addCommand( 'undo', {
33 if ( undoManager
.undo() ) {
34 editor
.selectionChange();
35 this.fire( 'afterUndo' );
42 var redoCommand
= editor
.addCommand( 'redo', {
44 if ( undoManager
.redo() ) {
45 editor
.selectionChange();
46 this.fire( 'afterRedo' );
53 editor
.setKeystroke( [
54 [ keystrokes
[ 0 ], 'undo' ],
55 [ keystrokes
[ 1 ], 'redo' ],
56 [ keystrokes
[ 2 ], 'redo' ]
59 undoManager
.onChange = function() {
60 undoCommand
.setState( undoManager
.undoable() ? CKEDITOR
.TRISTATE_OFF : CKEDITOR
.TRISTATE_DISABLED
);
61 redoCommand
.setState( undoManager
.redoable() ? CKEDITOR
.TRISTATE_OFF : CKEDITOR
.TRISTATE_DISABLED
);
64 function recordCommand( event
) {
65 // If the command hasn't been marked to not support undo.
66 if ( undoManager
.enabled
&& event
.data
.command
.canUndo
!== false )
70 // We'll save snapshots before and after executing a command.
71 editor
.on( 'beforeCommandExec', recordCommand
);
72 editor
.on( 'afterCommandExec', recordCommand
);
74 // Save snapshots before doing custom changes.
75 editor
.on( 'saveSnapshot', function( evt
) {
76 undoManager
.save( evt
.data
&& evt
.data
.contentOnly
);
79 // Event manager listeners should be attached on contentDom.
80 editor
.on( 'contentDom', editingHandler
.attachListeners
, editingHandler
);
82 editor
.on( 'instanceReady', function() {
83 // Saves initial snapshot.
84 editor
.fire( 'saveSnapshot' );
87 // Always save an undo snapshot - the previous mode might have
88 // changed editor contents.
89 editor
.on( 'beforeModeUnload', function() {
90 editor
.mode
== 'wysiwyg' && undoManager
.save( true );
93 function toggleUndoManager() {
94 undoManager
.enabled
= editor
.readOnly
? false : editor
.mode
== 'wysiwyg';
95 undoManager
.onChange();
98 // Make the undo manager available only in wysiwyg mode.
99 editor
.on( 'mode', toggleUndoManager
);
101 // Disable undo manager when in read-only mode.
102 editor
.on( 'readOnly', toggleUndoManager
);
104 if ( editor
.ui
.addButton
) {
105 editor
.ui
.addButton( 'Undo', {
106 label: editor
.lang
.undo
.undo
,
111 editor
.ui
.addButton( 'Redo', {
112 label: editor
.lang
.undo
.redo
,
119 * Resets the undo stack.
121 * @member CKEDITOR.editor
123 editor
.resetUndo = function() {
124 // Reset the undo stack.
127 // Create the first image.
128 editor
.fire( 'saveSnapshot' );
132 * Amends the top of the undo stack (last undo image) with the current DOM changes.
135 * editor.fire( 'saveSnapshot' );
136 * editor.document.body.append(...);
137 * // Makes new changes following the last undo snapshot a part of it.
138 * editor.fire( 'updateSnapshot' );
142 * @event updateSnapshot
143 * @member CKEDITOR.editor
144 * @param {CKEDITOR.editor} editor This editor instance.
146 editor
.on( 'updateSnapshot', function() {
147 if ( undoManager
.currentImage
)
148 undoManager
.update();
152 * Locks the undo manager to prevent any save/update operations.
154 * It is convenient to lock the undo manager before performing DOM operations
155 * that should not be recored (e.g. auto paragraphing).
157 * See {@link CKEDITOR.plugins.undo.UndoManager#lock} for more details.
159 * **Note:** In order to unlock the undo manager, {@link #unlockSnapshot} has to be fired
160 * the same number of times that `lockSnapshot` has been fired.
163 * @event lockSnapshot
164 * @member CKEDITOR.editor
165 * @param {CKEDITOR.editor} editor This editor instance.
167 * @param {Boolean} [data.dontUpdate] When set to `true`, the last snapshot will not be updated
168 * with the current content and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
169 * @param {Boolean} [data.forceUpdate] When set to `true`, the last snapshot will always be updated
170 * with the current content and selection. Read more in the {@link CKEDITOR.plugins.undo.UndoManager#lock} method.
172 editor
.on( 'lockSnapshot', function( evt
) {
174 undoManager
.lock( data
&& data
.dontUpdate
, data
&& data
.forceUpdate
);
178 * Unlocks the undo manager and updates the latest snapshot.
181 * @event unlockSnapshot
182 * @member CKEDITOR.editor
183 * @param {CKEDITOR.editor} editor This editor instance.
185 editor
.on( 'unlockSnapshot', undoManager
.unlock
, undoManager
);
189 CKEDITOR
.plugins
.undo
= {};
192 * Main logic for the Redo/Undo feature.
195 * @class CKEDITOR.plugins.undo.UndoManager
196 * @constructor Creates an UndoManager class instance.
197 * @param {CKEDITOR.editor} editor
199 var UndoManager
= CKEDITOR
.plugins
.undo
.UndoManager = function( editor
) {
201 * An array storing the number of key presses, count in a row. Use {@link #keyGroups} members as index.
203 * **Note:** The keystroke count will be reset after reaching the limit of characters per snapshot.
207 this.strokesRecorded
= [ 0, 0 ];
210 * When the `locked` property is not `null`, the undo manager is locked, so
211 * operations like `save` or `update` are forbidden.
213 * The manager can be locked and unlocked by the {@link #lock} and {@link #unlock}
214 * methods, respectively.
217 * @property {Object} [locked=null]
222 * Contains the previously processed key group, based on {@link #keyGroups}.
223 * `-1` means an unknown group.
227 * @property {Number} [previousKeyGroup=-1]
229 this.previousKeyGroup
= -1;
232 * The maximum number of snapshots in the stack. Configurable via {@link CKEDITOR.config#undoStackSize}.
235 * @property {Number} [limit]
237 this.limit
= editor
.config
.undoStackSize
|| 20;
240 * The maximum number of characters typed/deleted in one undo step.
245 this.strokesLimit
= 25;
247 this.editor
= editor
;
249 // Reset the undo stack.
253 UndoManager
.prototype = {
255 * Handles keystroke support for the undo manager. It is called on `keyup` event for
256 * keystrokes that can change the editor content.
258 * @param {Number} keyCode The key code.
259 * @param {Boolean} [strokesPerSnapshotExceeded] When set to `true`, the method will
260 * behave as if the strokes limit was exceeded regardless of the {@link #strokesRecorded} value.
262 type: function( keyCode
, strokesPerSnapshotExceeded
) {
263 var keyGroup
= UndoManager
.getKeyGroup( keyCode
),
264 // Count of keystrokes in current a row.
265 // Note if strokesPerSnapshotExceeded will be exceeded, it'll be restarted.
266 strokesRecorded
= this.strokesRecorded
[ keyGroup
] + 1;
268 strokesPerSnapshotExceeded
=
269 ( strokesPerSnapshotExceeded
|| strokesRecorded
>= this.strokesLimit
);
272 onTypingStart( this );
274 if ( strokesPerSnapshotExceeded
) {
275 // Reset the count of strokes, so it'll be later assigned to this.strokesRecorded.
278 this.editor
.fire( 'saveSnapshot' );
280 // Fire change event.
281 this.editor
.fire( 'change' );
284 // Store recorded strokes count.
285 this.strokesRecorded
[ keyGroup
] = strokesRecorded
;
286 // This prop will tell in next itaration what kind of group was processed previously.
287 this.previousKeyGroup
= keyGroup
;
291 * Whether the new `keyCode` belongs to a different group than the previous one ({@link #previousKeyGroup}).
294 * @param {Number} keyCode
297 keyGroupChanged: function( keyCode
) {
298 return UndoManager
.getKeyGroup( keyCode
) != this.previousKeyGroup
;
302 * Resets the undo stack.
305 // Stack for all the undo and redo snapshots, they're always created/removed
309 // Current snapshot history index.
312 this.currentImage
= null;
314 this.hasUndo
= false;
315 this.hasRedo
= false;
322 * Resets all typing variables.
326 resetType: function() {
327 this.strokesRecorded
= [ 0, 0 ];
329 this.previousKeyGroup
= -1;
333 * Refreshes the state of the {@link CKEDITOR.plugins.undo.UndoManager undo manager}
334 * as well as the state of the `undo` and `redo` commands.
336 refreshState: function() {
337 // These lines can be handled within onChange() too.
338 this.hasUndo
= !!this.getNextImage( true );
339 this.hasRedo
= !!this.getNextImage( false );
346 * Saves a snapshot of the document image for later retrieval.
348 * @param {Boolean} onContentOnly If set to `true`, the snapshot will be saved only if the content has changed.
349 * @param {CKEDITOR.plugins.undo.Image} image An optional image to save. If skipped, current editor will be used.
350 * @param {Boolean} [autoFireChange=true] If set to `false`, will not trigger the {@link CKEDITOR.editor#change} event to editor.
352 save: function( onContentOnly
, image
, autoFireChange
) {
353 var editor
= this.editor
;
354 // Do not change snapshots stack when locked, editor is not ready,
355 // editable is not ready or when editor is in mode difference than 'wysiwyg'.
356 if ( this.locked
|| editor
.status
!= 'ready' || editor
.mode
!= 'wysiwyg' )
359 var editable
= editor
.editable();
360 if ( !editable
|| editable
.status
!= 'ready' )
363 var snapshots
= this.snapshots
;
365 // Get a content image.
367 image
= new Image( editor
);
369 // Do nothing if it was not possible to retrieve an image.
370 if ( image
.contents
=== false )
373 // Check if this is a duplicate. In such case, do nothing.
374 if ( this.currentImage
) {
375 if ( image
.equalsContent( this.currentImage
) ) {
379 if ( image
.equalsSelection( this.currentImage
) )
381 } else if ( autoFireChange
!== false ) {
382 editor
.fire( 'change' );
386 // Drop future snapshots.
387 snapshots
.splice( this.index
+ 1, snapshots
.length
- this.index
- 1 );
389 // If we have reached the limit, remove the oldest one.
390 if ( snapshots
.length
== this.limit
)
393 // Add the new image, updating the current index.
394 this.index
= snapshots
.push( image
) - 1;
396 this.currentImage
= image
;
398 if ( autoFireChange
!== false )
404 * Sets editor content/selection to the one stored in `image`.
406 * @param {CKEDITOR.plugins.undo.Image} image
408 restoreImage: function( image
) {
409 // Bring editor focused to restore selection.
410 var editor
= this.editor
,
413 if ( image
.bookmarks
) {
415 // Retrieve the selection beforehand. (#8324)
416 sel
= editor
.getSelection();
419 // Start transaction - do not allow any mutations to the
420 // snapshots stack done when selecting bookmarks (much probably
421 // by selectionChange listener).
422 this.locked
= { level: 999 };
424 this.editor
.loadSnapshot( image
.contents
);
426 if ( image
.bookmarks
)
427 sel
.selectBookmarks( image
.bookmarks
);
428 else if ( CKEDITOR
.env
.ie
) {
429 // IE BUG: If I don't set the selection to *somewhere* after setting
430 // document contents, then IE would create an empty paragraph at the bottom
431 // the next time the document is modified.
432 var $range
= this.editor
.document
.getBody().$.createTextRange();
433 $range
.collapse( true );
439 this.index
= image
.index
;
440 this.currentImage
= this.snapshots
[ this.index
];
442 // Update current image with the actual editor
443 // content, since actualy content may differ from
444 // the original snapshot due to dom change. (#4622)
448 editor
.fire( 'change' );
452 * Gets the closest available image.
454 * @param {Boolean} isUndo If `true`, it will return the previous image.
455 * @returns {CKEDITOR.plugins.undo.Image} Next image or `null`.
457 getNextImage: function( isUndo
) {
458 var snapshots
= this.snapshots
,
459 currentImage
= this.currentImage
,
462 if ( currentImage
) {
464 for ( i
= this.index
- 1; i
>= 0; i
-- ) {
465 image
= snapshots
[ i
];
466 if ( !currentImage
.equalsContent( image
) ) {
472 for ( i
= this.index
+ 1; i
< snapshots
.length
; i
++ ) {
473 image
= snapshots
[ i
];
474 if ( !currentImage
.equalsContent( image
) ) {
486 * Checks the current redo state.
488 * @returns {Boolean} Whether the document has a previous state to retrieve.
490 redoable: function() {
491 return this.enabled
&& this.hasRedo
;
495 * Checks the current undo state.
497 * @returns {Boolean} Whether the document has a future state to restore.
499 undoable: function() {
500 return this.enabled
&& this.hasUndo
;
504 * Performs an undo operation on current index.
507 if ( this.undoable() ) {
510 var image
= this.getNextImage( true );
512 return this.restoreImage( image
), true;
519 * Performs a redo operation on current index.
522 if ( this.redoable() ) {
523 // Try to save. If no changes have been made, the redo stack
524 // will not change, so it will still be redoable.
527 // If instead we had changes, we can't redo anymore.
528 if ( this.redoable() ) {
529 var image
= this.getNextImage( false );
531 return this.restoreImage( image
), true;
539 * Updates the last snapshot of the undo stack with the current editor content.
541 * @param {CKEDITOR.plugins.undo.Image} [newImage] The image which will replace the current one.
542 * If it is not set, it defaults to the image taken from the editor.
544 update: function( newImage
) {
545 // Do not change snapshots stack is locked.
550 newImage
= new Image( this.editor
);
553 snapshots
= this.snapshots
;
555 // Find all previous snapshots made for the same content (which differ
556 // only by selection) and replace all of them with the current image.
557 while ( i
> 0 && this.currentImage
.equalsContent( snapshots
[ i
- 1 ] ) )
560 snapshots
.splice( i
, this.index
- i
+ 1, newImage
);
562 this.currentImage
= newImage
;
566 * Amends the last snapshot and changes its selection (only in case when content
567 * is equal between these two).
570 * @param {CKEDITOR.plugins.undo.Image} newSnapshot New snapshot with new selection.
571 * @returns {Boolean} Returns `true` if selection was amended.
573 updateSelection: function( newSnapshot
) {
574 if ( !this.snapshots
.length
)
577 var snapshots
= this.snapshots
,
578 lastImage
= snapshots
[ snapshots
.length
- 1 ];
580 if ( lastImage
.equalsContent( newSnapshot
) ) {
581 if ( !lastImage
.equalsSelection( newSnapshot
) ) {
582 snapshots
[ snapshots
.length
- 1 ] = newSnapshot
;
583 this.currentImage
= newSnapshot
;
592 * Locks the snapshot stack to prevent any save/update operations and when necessary,
593 * updates the tip of the snapshot stack with the DOM changes introduced during the
594 * locked period, after the {@link #unlock} method is called.
596 * It is mainly used to ensure any DOM operations that should not be recorded
597 * (e.g. auto paragraphing) are not added to the stack.
599 * **Note:** For every `lock` call you must call {@link #unlock} once to unlock the undo manager.
602 * @param {Boolean} [dontUpdate] When set to `true`, the last snapshot will not be updated
603 * with current content and selection. By default, if undo manager was up to date when the lock started,
604 * the last snapshot will be updated to the current state when unlocking. This means that all changes
605 * done during the lock will be merged into the previous snapshot or the next one. Use this option to gain
606 * more control over this behavior. For example, it is possible to group changes done during the lock into
607 * a separate snapshot.
608 * @param {Boolean} [forceUpdate] When set to `true`, the last snapshot will always be updated with the
609 * current content and selection regardless of the current state of the undo manager.
610 * When not set, the last snapshot will be updated only if the undo manager was up to date when locking.
611 * Additionally, this option makes it possible to lock the snapshot when the editor is not in the `wysiwyg` mode,
612 * because when it is passed, the snapshots will not need to be compared.
614 lock: function( dontUpdate
, forceUpdate
) {
615 if ( !this.locked
) {
617 this.locked
= { level: 1 };
624 // Make a contents image. Don't include bookmarks, because:
625 // * we don't compare them,
626 // * there's a chance that DOM has been changed since
627 // locked (e.g. fake) selection was made, so createBookmark2 could fail.
628 // http://dev.ckeditor.com/ticket/11027#comment:3
629 var imageBefore
= new Image( this.editor
, true );
631 // If current editor content matches the tip of snapshot stack,
632 // the stack tip must be updated by unlock, to include any changes made
633 // during this period.
634 if ( this.currentImage
&& this.currentImage
.equalsContent( imageBefore
) )
635 update
= imageBefore
;
638 this.locked
= { update: update
, level: 1 };
641 // Increase the level of lock.
648 * Unlocks the snapshot stack and checks to amend the last snapshot.
650 * See {@link #lock} for more details.
656 // Decrease level of lock and check if equals 0, what means that undoM is completely unlocked.
657 if ( !--this.locked
.level
) {
658 var update
= this.locked
.update
;
662 // forceUpdate was passed to lock().
663 if ( update
=== true )
665 // update is instance of Image.
667 var newImage
= new Image( this.editor
, true );
669 if ( !update
.equalsContent( newImage
) )
678 * Codes for navigation keys like *Arrows*, *Page Up/Down*, etc.
679 * Used by the {@link #isNavigationKey} method.
685 UndoManager
.navigationKeyCodes
= {
686 37: 1, 38: 1, 39: 1, 40: 1, // Arrows.
687 36: 1, 35: 1, // Home, End.
688 33: 1, 34: 1 // PgUp, PgDn.
692 * Key groups identifier mapping. Used for accessing members in
693 * {@link #strokesRecorded}.
695 * * `FUNCTIONAL` – identifier for the *Backspace* / *Delete* key.
696 * * `PRINTABLE` – identifier for printable keys.
700 * undoManager.strokesRecorded[ undoManager.keyGroups.FUNCTIONAL ];
706 UndoManager
.keyGroups
= {
712 * Checks whether a key is one of navigation keys (*Arrows*, *Page Up/Down*, etc.).
713 * See also the {@link #navigationKeyCodes} property.
717 * @param {Number} keyCode
720 UndoManager
.isNavigationKey = function( keyCode
) {
721 return !!UndoManager
.navigationKeyCodes
[ keyCode
];
725 * Returns the group to which the passed `keyCode` belongs.
729 * @param {Number} keyCode
732 UndoManager
.getKeyGroup = function( keyCode
) {
733 var keyGroups
= UndoManager
.keyGroups
;
735 return backspaceOrDelete
[ keyCode
] ? keyGroups
.FUNCTIONAL : keyGroups
.PRINTABLE
;
741 * @param {Number} keyGroup
744 UndoManager
.getOppositeKeyGroup = function( keyGroup
) {
745 var keyGroups
= UndoManager
.keyGroups
;
746 return ( keyGroup
== keyGroups
.FUNCTIONAL
? keyGroups
.PRINTABLE : keyGroups
.FUNCTIONAL
);
750 * Whether we need to use a workaround for functional (*Backspace*, *Delete*) keys not firing
751 * the `keypress` event in Internet Explorer in this environment and for the specified `keyCode`.
755 * @param {Number} keyCode
758 UndoManager
.ieFunctionalKeysBug = function( keyCode
) {
759 return CKEDITOR
.env
.ie
&& UndoManager
.getKeyGroup( keyCode
) == UndoManager
.keyGroups
.FUNCTIONAL
;
762 // Helper method called when undoManager.typing val was changed to true.
763 function onTypingStart( undoManager
) {
764 // It's safe to now indicate typing state.
765 undoManager
.typing
= true;
767 // Manually mark snapshot as available.
768 undoManager
.hasUndo
= true;
769 undoManager
.hasRedo
= false;
771 undoManager
.onChange();
775 * Contains a snapshot of the editor content and selection at a given point in time.
778 * @class CKEDITOR.plugins.undo.Image
779 * @constructor Creates an Image class instance.
780 * @param {CKEDITOR.editor} editor The editor instance on which the image is created.
781 * @param {Boolean} [contentsOnly] If set to `true`, the image will only contain content without the selection.
783 var Image
= CKEDITOR
.plugins
.undo
.Image = function( editor
, contentsOnly
) {
784 this.editor
= editor
;
786 editor
.fire( 'beforeUndoImage' );
788 var contents
= editor
.getSnapshot();
790 // In IE, we need to remove the expando attributes.
791 if ( CKEDITOR
.env
.ie
&& contents
)
792 contents
= contents
.replace( /\s+data-cke-expando=".*?"/g, '' );
794 this.contents
= contents
;
796 if ( !contentsOnly
) {
797 var selection
= contents
&& editor
.getSelection();
798 this.bookmarks
= selection
&& selection
.createBookmarks2( true );
801 editor
.fire( 'afterUndoImage' );
804 // Attributes that browser may changing them when setting via innerHTML.
805 var protectedAttrs
= /\b(?:href|src|name)="[^"]*?"/gi;
809 * @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to.
810 * @returns {Boolean} Returns `true` if content in `otherImage` is the same.
812 equalsContent: function( otherImage
) {
813 var thisContents
= this.contents
,
814 otherContents
= otherImage
.contents
;
816 // For IE7 and IE QM: Comparing only the protected attribute values but not the original ones.(#4522)
817 if ( CKEDITOR
.env
.ie
&& ( CKEDITOR
.env
.ie7Compat
|| CKEDITOR
.env
.quirks
) ) {
818 thisContents
= thisContents
.replace( protectedAttrs
, '' );
819 otherContents
= otherContents
.replace( protectedAttrs
, '' );
822 if ( thisContents
!= otherContents
)
829 * @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to.
830 * @returns {Boolean} Returns `true` if selection in `otherImage` is the same.
832 equalsSelection: function( otherImage
) {
833 var bookmarksA
= this.bookmarks
,
834 bookmarksB
= otherImage
.bookmarks
;
836 if ( bookmarksA
|| bookmarksB
) {
837 if ( !bookmarksA
|| !bookmarksB
|| bookmarksA
.length
!= bookmarksB
.length
)
840 for ( var i
= 0; i
< bookmarksA
.length
; i
++ ) {
841 var bookmarkA
= bookmarksA
[ i
],
842 bookmarkB
= bookmarksB
[ i
];
844 if ( bookmarkA
.startOffset
!= bookmarkB
.startOffset
|| bookmarkA
.endOffset
!= bookmarkB
.endOffset
||
845 !CKEDITOR
.tools
.arrayCompare( bookmarkA
.start
, bookmarkB
.start
) ||
846 !CKEDITOR
.tools
.arrayCompare( bookmarkA
.end
, bookmarkB
.end
) ) {
859 * @property {String} contents
863 * Bookmarks representing the selection in an image.
866 * @property {Object[]} bookmarks Array of bookmark2 objects, see {@link CKEDITOR.dom.range#createBookmark2} for definition.
871 * A class encapsulating all native event listeners which have to be used in
872 * order to handle undo manager integration for native editing actions (excluding drag and drop and paste support
873 * handled by the Clipboard plugin).
877 * @class CKEDITOR.plugins.undo.NativeEditingHandler
878 * @member CKEDITOR.plugins.undo Undo manager owning the handler.
880 * @param {CKEDITOR.plugins.undo.UndoManager} undoManager
882 var NativeEditingHandler
= CKEDITOR
.plugins
.undo
.NativeEditingHandler = function( undoManager
) {
883 // We'll use keyboard + input events to determine if snapshot should be created.
884 // Since `input` event is fired before `keyup`. We can tell in `keyup` event if input occured.
885 // That will tell us if any printable data was inserted.
886 // On `input` event we'll increase input fired counter for proper key code.
887 // Eventually it might be canceled by paste/drop using `ignoreInputEvent` flag.
888 // Order of events can be found in http://www.w3.org/TR/DOM-Level-3-Events/
891 * An undo manager instance owning the editing handler.
893 * @property {CKEDITOR.plugins.undo.UndoManager} undoManager
895 this.undoManager
= undoManager
;
898 * See {@link #ignoreInputEventListener}.
903 this.ignoreInputEvent
= false;
906 * A stack of pressed keys.
909 * @property {CKEDITOR.plugins.undo.KeyEventsStack} keyEventsStack
911 this.keyEventsStack
= new KeyEventsStack();
914 * An image of the editor during the `keydown` event (therefore without DOM modification).
916 * @property {CKEDITOR.plugins.undo.Image} lastKeydownImage
918 this.lastKeydownImage
= null;
921 NativeEditingHandler
.prototype = {
923 * The `keydown` event listener.
925 * @param {CKEDITOR.dom.event} evt
927 onKeydown: function( evt
) {
928 var keyCode
= evt
.data
.getKey();
930 // The composition is in progress - ignore the key. (#12597)
931 if ( keyCode
=== 229 ) {
935 // Block undo/redo keystrokes when at the bottom/top of the undo stack (#11126 and #11677).
936 if ( CKEDITOR
.tools
.indexOf( keystrokes
, evt
.data
.getKeystroke() ) > -1 ) {
937 evt
.data
.preventDefault();
941 // Cleaning tab functional keys.
942 this.keyEventsStack
.cleanUp( evt
);
944 var undoManager
= this.undoManager
;
946 // Gets last record for provided keyCode. If not found will create one.
947 var last
= this.keyEventsStack
.getLast( keyCode
);
949 this.keyEventsStack
.push( keyCode
);
952 // We need to store an image which will be used in case of key group
954 this.lastKeydownImage
= new Image( undoManager
.editor
);
956 if ( UndoManager
.isNavigationKey( keyCode
) || this.undoManager
.keyGroupChanged( keyCode
) ) {
957 if ( undoManager
.strokesRecorded
[ 0 ] || undoManager
.strokesRecorded
[ 1 ] ) {
958 // We already have image, so we'd like to reuse it.
961 undoManager
.save( false, this.lastKeydownImage
, false );
962 undoManager
.resetType();
968 * The `input` event listener.
970 onInput: function() {
971 // Input event is ignored if paste/drop event were fired before.
972 if ( this.ignoreInputEvent
) {
973 // Reset flag - ignore only once.
974 this.ignoreInputEvent
= false;
978 var lastInput
= this.keyEventsStack
.getLast();
979 // Nothing in key events stack, but input event called. Interesting...
980 // That's because on Android order of events is buggy and also keyCode is set to 0.
982 lastInput
= this.keyEventsStack
.push( 0 );
985 // Increment inputs counter for provided key code.
986 this.keyEventsStack
.increment( lastInput
.keyCode
);
989 if ( this.keyEventsStack
.getTotalInputs() >= this.undoManager
.strokesLimit
) {
990 this.undoManager
.type( lastInput
.keyCode
, true );
991 this.keyEventsStack
.resetInputs();
996 * The `keyup` event listener.
998 * @param {CKEDITOR.dom.event} evt
1000 onKeyup: function( evt
) {
1001 var undoManager
= this.undoManager
,
1002 keyCode
= evt
.data
.getKey(),
1003 totalInputs
= this.keyEventsStack
.getTotalInputs();
1005 // Remove record from stack for provided key code.
1006 this.keyEventsStack
.remove( keyCode
);
1008 // Second part of the workaround for IEs functional keys bug. We need to check whether something has really
1009 // changed because we blindly mocked the keypress event.
1010 // Also we need to be aware that lastKeydownImage might not be available (#12327).
1011 if ( UndoManager
.ieFunctionalKeysBug( keyCode
) && this.lastKeydownImage
&&
1012 this.lastKeydownImage
.equalsContent( new Image( undoManager
.editor
, true ) ) ) {
1016 if ( totalInputs
> 0 ) {
1017 undoManager
.type( keyCode
);
1018 } else if ( UndoManager
.isNavigationKey( keyCode
) ) {
1019 // Note content snapshot has been checked in keydown.
1020 this.onNavigationKey( true );
1025 * Method called for navigation change. At first it will check if current content does not differ
1026 * from the last saved snapshot.
1028 * * If the content is different, the method creates a standard, extra snapshot.
1029 * * If the content is not different, the method will compare the selection, and will
1030 * amend the last snapshot selection if it changed.
1032 * @param {Boolean} skipContentCompare If set to `true`, it will not compare content, and only do a selection check.
1034 onNavigationKey: function( skipContentCompare
) {
1035 var undoManager
= this.undoManager
;
1037 // We attempt to save content snapshot, if content didn't change, we'll
1038 // only amend selection.
1039 if ( skipContentCompare
|| !undoManager
.save( true, null, false ) )
1040 undoManager
.updateSelection( new Image( undoManager
.editor
) );
1042 undoManager
.resetType();
1046 * Makes the next `input` event to be ignored.
1048 ignoreInputEventListener: function() {
1049 this.ignoreInputEvent
= true;
1053 * Attaches editable listeners required to provide the undo functionality.
1055 attachListeners: function() {
1056 var editor
= this.undoManager
.editor
,
1057 editable
= editor
.editable(),
1060 // We'll create a snapshot here (before DOM modification), because we'll
1061 // need unmodified content when we got keygroup toggled in keyup.
1062 editable
.attachListener( editable
, 'keydown', function( evt
) {
1063 that
.onKeydown( evt
);
1065 // On IE keypress isn't fired for functional (backspace/delete) keys.
1066 // Let's pretend that something's changed.
1067 if ( UndoManager
.ieFunctionalKeysBug( evt
.data
.getKey() ) ) {
1070 }, null, null, 999 );
1072 // Only IE can't use input event, because it's not fired in contenteditable.
1073 editable
.attachListener( editable
, ( CKEDITOR
.env
.ie
? 'keypress' : 'input' ), that
.onInput
, that
, null, 999 );
1075 // Keyup executes main snapshot logic.
1076 editable
.attachListener( editable
, 'keyup', that
.onKeyup
, that
, null, 999 );
1078 // On paste and drop we need to ignore input event.
1079 // It would result with calling undoManager.type() on any following key.
1080 editable
.attachListener( editable
, 'paste', that
.ignoreInputEventListener
, that
, null, 999 );
1081 editable
.attachListener( editable
, 'drop', that
.ignoreInputEventListener
, that
, null, 999 );
1083 // Click should create a snapshot if needed, but shouldn't cause change event.
1084 // Don't pass onNavigationKey directly as a listener because it accepts one argument which
1085 // will conflict with evt passed to listener.
1087 editable
.attachListener( editable
.isInline() ? editable : editor
.document
.getDocumentElement(), 'click', function() {
1088 that
.onNavigationKey();
1089 }, null, null, 999 );
1091 // When pressing `Tab` key while editable is focused, `keyup` event is not fired.
1092 // Which means that record for `tab` key stays in key events stack.
1093 // We assume that when editor is blurred `tab` key is already up.
1094 editable
.attachListener( this.undoManager
.editor
, 'blur', function() {
1095 that
.keyEventsStack
.remove( 9 /*Tab*/ );
1096 }, null, null, 999 );
1101 * This class represents a stack of pressed keys and stores information
1102 * about how many `input` events each key press has caused.
1106 * @class CKEDITOR.plugins.undo.KeyEventsStack
1109 var KeyEventsStack
= CKEDITOR
.plugins
.undo
.KeyEventsStack = function() {
1116 KeyEventsStack
.prototype = {
1118 * Pushes a literal object with two keys: `keyCode` and `inputs` (whose initial value is set to `0`) to stack.
1119 * It is intended to be called on the `keydown` event.
1121 * @param {Number} keyCode
1123 push: function( keyCode
) {
1124 var length
= this.stack
.push( { keyCode: keyCode
, inputs: 0 } );
1125 return this.stack
[ length
- 1 ];
1129 * Returns the index of the last registered `keyCode` in the stack.
1130 * If no `keyCode` is provided, then the function will return the index of the last item.
1131 * If an item is not found, it will return `-1`.
1133 * @param {Number} [keyCode]
1136 getLastIndex: function( keyCode
) {
1137 if ( typeof keyCode
!= 'number' ) {
1138 return this.stack
.length
- 1; // Last index or -1.
1140 var i
= this.stack
.length
;
1142 if ( this.stack
[ i
].keyCode
== keyCode
) {
1151 * Returns the last key recorded in the stack. If `keyCode` is provided, then it will return
1152 * the last record for this `keyCode`.
1154 * @param {Number} [keyCode]
1155 * @returns {Object} Last matching record or `null`.
1157 getLast: function( keyCode
) {
1158 var index
= this.getLastIndex( keyCode
);
1159 if ( index
!= -1 ) {
1160 return this.stack
[ index
];
1167 * Increments registered input events for stack record for a given `keyCode`.
1169 * @param {Number} keyCode
1171 increment: function( keyCode
) {
1172 var found
= this.getLast( keyCode
);
1173 if ( !found
) { // %REMOVE_LINE%
1174 throw new Error( 'Trying to increment, but could not found by keyCode: ' + keyCode
+ '.' ); // %REMOVE_LINE%
1181 * Removes the last record from the stack for the provided `keyCode`.
1183 * @param {Number} keyCode
1185 remove: function( keyCode
) {
1186 var index
= this.getLastIndex( keyCode
);
1188 if ( index
!= -1 ) {
1189 this.stack
.splice( index
, 1 );
1194 * Resets the `inputs` value to `0` for a given `keyCode` or in entire stack if a
1195 * `keyCode` is not specified.
1197 * @param {Number} [keyCode]
1199 resetInputs: function( keyCode
) {
1200 if ( typeof keyCode
== 'number' ) {
1201 var last
= this.getLast( keyCode
);
1203 if ( !last
) { // %REMOVE_LINE%
1204 throw new Error( 'Trying to reset inputs count, but could not found by keyCode: ' + keyCode
+ '.' ); // %REMOVE_LINE%
1209 var i
= this.stack
.length
;
1211 this.stack
[ i
].inputs
= 0;
1217 * Sums up inputs number for each key code and returns it.
1221 getTotalInputs: function() {
1222 var i
= this.stack
.length
,
1226 total
+= this.stack
[ i
].inputs
;
1232 * Cleans the stack based on a provided `keydown` event object. The rationale behind this method
1233 * is that some keystrokes cause the `keydown` event to be fired in the editor, but not the `keyup` event.
1234 * For instance, *Alt+Tab* will fire `keydown`, but since the editor is blurred by it, then there is
1235 * no `keyup`, so the keystroke is not removed from the stack.
1237 * @param {CKEDITOR.dom.event} event
1239 cleanUp: function( event
) {
1240 var nativeEvent
= event
.data
.$;
1242 if ( !( nativeEvent
.ctrlKey
|| nativeEvent
.metaKey
) ) {
1245 if ( !nativeEvent
.shiftKey
) {
1248 if ( !nativeEvent
.altKey
) {
1256 * The number of undo steps to be saved. The higher value is set, the more
1257 * memory is used for it.
1259 * config.undoStackSize = 50;
1261 * @cfg {Number} [undoStackSize=20]
1262 * @member CKEDITOR.config
1266 * Fired when the editor is about to save an undo snapshot. This event can be
1267 * fired by plugins and customizations to make the editor save undo snapshots.
1269 * @event saveSnapshot
1270 * @member CKEDITOR.editor
1271 * @param {CKEDITOR.editor} editor This editor instance.
1275 * Fired before an undo image is to be created. An *undo image* represents the
1276 * editor state at some point. It is saved into the undo store, so the editor is
1277 * able to recover the editor state on undo and redo operations.
1280 * @event beforeUndoImage
1281 * @member CKEDITOR.editor
1282 * @param {CKEDITOR.editor} editor This editor instance.
1283 * @see CKEDITOR.editor#afterUndoImage
1287 * Fired after an undo image is created. An *undo image* represents the
1288 * editor state at some point. It is saved into the undo store, so the editor is
1289 * able to recover the editor state on undo and redo operations.
1292 * @event afterUndoImage
1293 * @member CKEDITOR.editor
1294 * @param {CKEDITOR.editor} editor This editor instance.
1295 * @see CKEDITOR.editor#beforeUndoImage
1299 * Fired when the content of the editor is changed.
1301 * Due to performance reasons, it is not verified if the content really changed.
1302 * The editor instead watches several editing actions that usually result in
1303 * changes. This event may thus in some cases be fired when no changes happen
1304 * or may even get fired twice.
1306 * If it is important not to get the `change` event fired too often, you should compare the
1307 * previous and the current editor content inside the event listener. It is
1308 * not recommended to do that on every `change` event.
1310 * Please note that the `change` event is only fired in the {@link #property-mode wysiwyg mode}.
1311 * In order to implement similar functionality in the source mode, you can listen for example to the {@link #key}
1312 * event or the native [`input`](https://developer.mozilla.org/en-US/docs/Web/Reference/Events/input)
1313 * event (not supported by Internet Explorer 8).
1315 * editor.on( 'mode', function() {
1316 * if ( this.mode == 'source' ) {
1317 * var editable = editor.editable();
1318 * editable.attachListener( editable, 'input', function() {
1319 * // Handle changes made in the source mode.
1326 * @member CKEDITOR.editor
1327 * @param {CKEDITOR.editor} editor This editor instance.