diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-01-25 17:45:33 +0100 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-01-25 18:00:33 +0100 |
commit | 7adcb81e4f83f98c468889aaa5a85558ba88c770 (patch) | |
tree | 0d6ede733777b29060b48df4afaa2c64bfbae276 /sources/plugins/undo/plugin.js | |
download | connexionswing-ckeditor-component-7adcb81e4f83f98c468889aaa5a85558ba88c770.tar.gz connexionswing-ckeditor-component-7adcb81e4f83f98c468889aaa5a85558ba88c770.tar.zst connexionswing-ckeditor-component-7adcb81e4f83f98c468889aaa5a85558ba88c770.zip |
Initial commit4.5.6
Diffstat (limited to 'sources/plugins/undo/plugin.js')
-rw-r--r-- | sources/plugins/undo/plugin.js | 1328 |
1 files changed, 1328 insertions, 0 deletions
diff --git a/sources/plugins/undo/plugin.js b/sources/plugins/undo/plugin.js new file mode 100644 index 00000000..d5479936 --- /dev/null +++ b/sources/plugins/undo/plugin.js | |||
@@ -0,0 +1,1328 @@ | |||
1 | /** | ||
2 | * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved. | ||
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | ||
4 | */ | ||
5 | |||
6 | /** | ||
7 | * @fileOverview Undo/Redo system for saving a shapshot for document modification | ||
8 | * and other recordable changes. | ||
9 | */ | ||
10 | |||
11 | 'use strict'; | ||
12 | |||
13 | ( function() { | ||
14 | var keystrokes = [ | ||
15 | CKEDITOR.CTRL + 90 /*Z*/, | ||
16 | CKEDITOR.CTRL + 89 /*Y*/, | ||
17 | CKEDITOR.CTRL + CKEDITOR.SHIFT + 90 /*Z*/ | ||
18 | ], | ||
19 | backspaceOrDelete = { 8: 1, 46: 1 }; | ||
20 | |||
21 | CKEDITOR.plugins.add( 'undo', { | ||
22 | // jscs:disable maximumLineLength | ||
23 | lang: 'af,ar,bg,bn,bs,ca,cs,cy,da,de,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 ); | ||
30 | |||
31 | var undoCommand = editor.addCommand( 'undo', { | ||
32 | exec: function() { | ||
33 | if ( undoManager.undo() ) { | ||
34 | editor.selectionChange(); | ||
35 | this.fire( 'afterUndo' ); | ||
36 | } | ||
37 | }, | ||
38 | startDisabled: true, | ||
39 | canUndo: false | ||
40 | } ); | ||
41 | |||
42 | var redoCommand = editor.addCommand( 'redo', { | ||
43 | exec: function() { | ||
44 | if ( undoManager.redo() ) { | ||
45 | editor.selectionChange(); | ||
46 | this.fire( 'afterRedo' ); | ||
47 | } | ||
48 | }, | ||
49 | startDisabled: true, | ||
50 | canUndo: false | ||
51 | } ); | ||
52 | |||
53 | editor.setKeystroke( [ | ||
54 | [ keystrokes[ 0 ], 'undo' ], | ||
55 | [ keystrokes[ 1 ], 'redo' ], | ||
56 | [ keystrokes[ 2 ], 'redo' ] | ||
57 | ] ); | ||
58 | |||
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 ); | ||
62 | }; | ||
63 | |||
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 ) | ||
67 | undoManager.save(); | ||
68 | } | ||
69 | |||
70 | // We'll save snapshots before and after executing a command. | ||
71 | editor.on( 'beforeCommandExec', recordCommand ); | ||
72 | editor.on( 'afterCommandExec', recordCommand ); | ||
73 | |||
74 | // Save snapshots before doing custom changes. | ||
75 | editor.on( 'saveSnapshot', function( evt ) { | ||
76 | undoManager.save( evt.data && evt.data.contentOnly ); | ||
77 | } ); | ||
78 | |||
79 | // Event manager listeners should be attached on contentDom. | ||
80 | editor.on( 'contentDom', editingHandler.attachListeners, editingHandler ); | ||
81 | |||
82 | editor.on( 'instanceReady', function() { | ||
83 | // Saves initial snapshot. | ||
84 | editor.fire( 'saveSnapshot' ); | ||
85 | } ); | ||
86 | |||
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 ); | ||
91 | } ); | ||
92 | |||
93 | function toggleUndoManager() { | ||
94 | undoManager.enabled = editor.readOnly ? false : editor.mode == 'wysiwyg'; | ||
95 | undoManager.onChange(); | ||
96 | } | ||
97 | |||
98 | // Make the undo manager available only in wysiwyg mode. | ||
99 | editor.on( 'mode', toggleUndoManager ); | ||
100 | |||
101 | // Disable undo manager when in read-only mode. | ||
102 | editor.on( 'readOnly', toggleUndoManager ); | ||
103 | |||
104 | if ( editor.ui.addButton ) { | ||
105 | editor.ui.addButton( 'Undo', { | ||
106 | label: editor.lang.undo.undo, | ||
107 | command: 'undo', | ||
108 | toolbar: 'undo,10' | ||
109 | } ); | ||
110 | |||
111 | editor.ui.addButton( 'Redo', { | ||
112 | label: editor.lang.undo.redo, | ||
113 | command: 'redo', | ||
114 | toolbar: 'undo,20' | ||
115 | } ); | ||
116 | } | ||
117 | |||
118 | /** | ||
119 | * Resets the undo stack. | ||
120 | * | ||
121 | * @member CKEDITOR.editor | ||
122 | */ | ||
123 | editor.resetUndo = function() { | ||
124 | // Reset the undo stack. | ||
125 | undoManager.reset(); | ||
126 | |||
127 | // Create the first image. | ||
128 | editor.fire( 'saveSnapshot' ); | ||
129 | }; | ||
130 | |||
131 | /** | ||
132 | * Amends the top of the undo stack (last undo image) with the current DOM changes. | ||
133 | * | ||
134 | * function() { | ||
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' ); | ||
139 | * .. | ||
140 | * } | ||
141 | * | ||
142 | * @event updateSnapshot | ||
143 | * @member CKEDITOR.editor | ||
144 | * @param {CKEDITOR.editor} editor This editor instance. | ||
145 | */ | ||
146 | editor.on( 'updateSnapshot', function() { | ||
147 | if ( undoManager.currentImage ) | ||
148 | undoManager.update(); | ||
149 | } ); | ||
150 | |||
151 | /** | ||
152 | * Locks the undo manager to prevent any save/update operations. | ||
153 | * | ||
154 | * It is convenient to lock the undo manager before performing DOM operations | ||
155 | * that should not be recored (e.g. auto paragraphing). | ||
156 | * | ||
157 | * See {@link CKEDITOR.plugins.undo.UndoManager#lock} for more details. | ||
158 | * | ||
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. | ||
161 | * | ||
162 | * @since 4.0 | ||
163 | * @event lockSnapshot | ||
164 | * @member CKEDITOR.editor | ||
165 | * @param {CKEDITOR.editor} editor This editor instance. | ||
166 | * @param data | ||
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. | ||
171 | */ | ||
172 | editor.on( 'lockSnapshot', function( evt ) { | ||
173 | var data = evt.data; | ||
174 | undoManager.lock( data && data.dontUpdate, data && data.forceUpdate ); | ||
175 | } ); | ||
176 | |||
177 | /** | ||
178 | * Unlocks the undo manager and updates the latest snapshot. | ||
179 | * | ||
180 | * @since 4.0 | ||
181 | * @event unlockSnapshot | ||
182 | * @member CKEDITOR.editor | ||
183 | * @param {CKEDITOR.editor} editor This editor instance. | ||
184 | */ | ||
185 | editor.on( 'unlockSnapshot', undoManager.unlock, undoManager ); | ||
186 | } | ||
187 | } ); | ||
188 | |||
189 | CKEDITOR.plugins.undo = {}; | ||
190 | |||
191 | /** | ||
192 | * Main logic for the Redo/Undo feature. | ||
193 | * | ||
194 | * @private | ||
195 | * @class CKEDITOR.plugins.undo.UndoManager | ||
196 | * @constructor Creates an UndoManager class instance. | ||
197 | * @param {CKEDITOR.editor} editor | ||
198 | */ | ||
199 | var UndoManager = CKEDITOR.plugins.undo.UndoManager = function( editor ) { | ||
200 | /** | ||
201 | * An array storing the number of key presses, count in a row. Use {@link #keyGroups} members as index. | ||
202 | * | ||
203 | * **Note:** The keystroke count will be reset after reaching the limit of characters per snapshot. | ||
204 | * | ||
205 | * @since 4.4.4 | ||
206 | */ | ||
207 | this.strokesRecorded = [ 0, 0 ]; | ||
208 | |||
209 | /** | ||
210 | * When the `locked` property is not `null`, the undo manager is locked, so | ||
211 | * operations like `save` or `update` are forbidden. | ||
212 | * | ||
213 | * The manager can be locked and unlocked by the {@link #lock} and {@link #unlock} | ||
214 | * methods, respectively. | ||
215 | * | ||
216 | * @readonly | ||
217 | * @property {Object} [locked=null] | ||
218 | */ | ||
219 | this.locked = null; | ||
220 | |||
221 | /** | ||
222 | * Contains the previously processed key group, based on {@link #keyGroups}. | ||
223 | * `-1` means an unknown group. | ||
224 | * | ||
225 | * @since 4.4.4 | ||
226 | * @readonly | ||
227 | * @property {Number} [previousKeyGroup=-1] | ||
228 | */ | ||
229 | this.previousKeyGroup = -1; | ||
230 | |||
231 | /** | ||
232 | * The maximum number of snapshots in the stack. Configurable via {@link CKEDITOR.config#undoStackSize}. | ||
233 | * | ||
234 | * @readonly | ||
235 | * @property {Number} [limit] | ||
236 | */ | ||
237 | this.limit = editor.config.undoStackSize || 20; | ||
238 | |||
239 | /** | ||
240 | * The maximum number of characters typed/deleted in one undo step. | ||
241 | * | ||
242 | * @since 4.4.5 | ||
243 | * @readonly | ||
244 | */ | ||
245 | this.strokesLimit = 25; | ||
246 | |||
247 | this.editor = editor; | ||
248 | |||
249 | // Reset the undo stack. | ||
250 | this.reset(); | ||
251 | }; | ||
252 | |||
253 | UndoManager.prototype = { | ||
254 | /** | ||
255 | * Handles keystroke support for the undo manager. It is called on `keyup` event for | ||
256 | * keystrokes that can change the editor content. | ||
257 | * | ||
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. | ||
261 | */ | ||
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; | ||
267 | |||
268 | strokesPerSnapshotExceeded = | ||
269 | ( strokesPerSnapshotExceeded || strokesRecorded >= this.strokesLimit ); | ||
270 | |||
271 | if ( !this.typing ) | ||
272 | onTypingStart( this ); | ||
273 | |||
274 | if ( strokesPerSnapshotExceeded ) { | ||
275 | // Reset the count of strokes, so it'll be later assigned to this.strokesRecorded. | ||
276 | strokesRecorded = 0; | ||
277 | |||
278 | this.editor.fire( 'saveSnapshot' ); | ||
279 | } else { | ||
280 | // Fire change event. | ||
281 | this.editor.fire( 'change' ); | ||
282 | } | ||
283 | |||
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; | ||
288 | }, | ||
289 | |||
290 | /** | ||
291 | * Whether the new `keyCode` belongs to a different group than the previous one ({@link #previousKeyGroup}). | ||
292 | * | ||
293 | * @since 4.4.5 | ||
294 | * @param {Number} keyCode | ||
295 | * @returns {Boolean} | ||
296 | */ | ||
297 | keyGroupChanged: function( keyCode ) { | ||
298 | return UndoManager.getKeyGroup( keyCode ) != this.previousKeyGroup; | ||
299 | }, | ||
300 | |||
301 | /** | ||
302 | * Resets the undo stack. | ||
303 | */ | ||
304 | reset: function() { | ||
305 | // Stack for all the undo and redo snapshots, they're always created/removed | ||
306 | // in consistency. | ||
307 | this.snapshots = []; | ||
308 | |||
309 | // Current snapshot history index. | ||
310 | this.index = -1; | ||
311 | |||
312 | this.currentImage = null; | ||
313 | |||
314 | this.hasUndo = false; | ||
315 | this.hasRedo = false; | ||
316 | this.locked = null; | ||
317 | |||
318 | this.resetType(); | ||
319 | }, | ||
320 | |||
321 | /** | ||
322 | * Resets all typing variables. | ||
323 | * | ||
324 | * @see #type | ||
325 | */ | ||
326 | resetType: function() { | ||
327 | this.strokesRecorded = [ 0, 0 ]; | ||
328 | this.typing = false; | ||
329 | this.previousKeyGroup = -1; | ||
330 | }, | ||
331 | |||
332 | /** | ||
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. | ||
335 | */ | ||
336 | refreshState: function() { | ||
337 | // These lines can be handled within onChange() too. | ||
338 | this.hasUndo = !!this.getNextImage( true ); | ||
339 | this.hasRedo = !!this.getNextImage( false ); | ||
340 | // Reset typing | ||
341 | this.resetType(); | ||
342 | this.onChange(); | ||
343 | }, | ||
344 | |||
345 | /** | ||
346 | * Saves a snapshot of the document image for later retrieval. | ||
347 | * | ||
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. | ||
351 | */ | ||
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' ) | ||
357 | return false; | ||
358 | |||
359 | var editable = editor.editable(); | ||
360 | if ( !editable || editable.status != 'ready' ) | ||
361 | return false; | ||
362 | |||
363 | var snapshots = this.snapshots; | ||
364 | |||
365 | // Get a content image. | ||
366 | if ( !image ) | ||
367 | image = new Image( editor ); | ||
368 | |||
369 | // Do nothing if it was not possible to retrieve an image. | ||
370 | if ( image.contents === false ) | ||
371 | return false; | ||
372 | |||
373 | // Check if this is a duplicate. In such case, do nothing. | ||
374 | if ( this.currentImage ) { | ||
375 | if ( image.equalsContent( this.currentImage ) ) { | ||
376 | if ( onContentOnly ) | ||
377 | return false; | ||
378 | |||
379 | if ( image.equalsSelection( this.currentImage ) ) | ||
380 | return false; | ||
381 | } else if ( autoFireChange !== false ) { | ||
382 | editor.fire( 'change' ); | ||
383 | } | ||
384 | } | ||
385 | |||
386 | // Drop future snapshots. | ||
387 | snapshots.splice( this.index + 1, snapshots.length - this.index - 1 ); | ||
388 | |||
389 | // If we have reached the limit, remove the oldest one. | ||
390 | if ( snapshots.length == this.limit ) | ||
391 | snapshots.shift(); | ||
392 | |||
393 | // Add the new image, updating the current index. | ||
394 | this.index = snapshots.push( image ) - 1; | ||
395 | |||
396 | this.currentImage = image; | ||
397 | |||
398 | if ( autoFireChange !== false ) | ||
399 | this.refreshState(); | ||
400 | return true; | ||
401 | }, | ||
402 | |||
403 | /** | ||
404 | * Sets editor content/selection to the one stored in `image`. | ||
405 | * | ||
406 | * @param {CKEDITOR.plugins.undo.Image} image | ||
407 | */ | ||
408 | restoreImage: function( image ) { | ||
409 | // Bring editor focused to restore selection. | ||
410 | var editor = this.editor, | ||
411 | sel; | ||
412 | |||
413 | if ( image.bookmarks ) { | ||
414 | editor.focus(); | ||
415 | // Retrieve the selection beforehand. (#8324) | ||
416 | sel = editor.getSelection(); | ||
417 | } | ||
418 | |||
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 }; | ||
423 | |||
424 | this.editor.loadSnapshot( image.contents ); | ||
425 | |||
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 ); | ||
434 | $range.select(); | ||
435 | } | ||
436 | |||
437 | this.locked = null; | ||
438 | |||
439 | this.index = image.index; | ||
440 | this.currentImage = this.snapshots[ this.index ]; | ||
441 | |||
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) | ||
445 | this.update(); | ||
446 | this.refreshState(); | ||
447 | |||
448 | editor.fire( 'change' ); | ||
449 | }, | ||
450 | |||
451 | /** | ||
452 | * Gets the closest available image. | ||
453 | * | ||
454 | * @param {Boolean} isUndo If `true`, it will return the previous image. | ||
455 | * @returns {CKEDITOR.plugins.undo.Image} Next image or `null`. | ||
456 | */ | ||
457 | getNextImage: function( isUndo ) { | ||
458 | var snapshots = this.snapshots, | ||
459 | currentImage = this.currentImage, | ||
460 | image, i; | ||
461 | |||
462 | if ( currentImage ) { | ||
463 | if ( isUndo ) { | ||
464 | for ( i = this.index - 1; i >= 0; i-- ) { | ||
465 | image = snapshots[ i ]; | ||
466 | if ( !currentImage.equalsContent( image ) ) { | ||
467 | image.index = i; | ||
468 | return image; | ||
469 | } | ||
470 | } | ||
471 | } else { | ||
472 | for ( i = this.index + 1; i < snapshots.length; i++ ) { | ||
473 | image = snapshots[ i ]; | ||
474 | if ( !currentImage.equalsContent( image ) ) { | ||
475 | image.index = i; | ||
476 | return image; | ||
477 | } | ||
478 | } | ||
479 | } | ||
480 | } | ||
481 | |||
482 | return null; | ||
483 | }, | ||
484 | |||
485 | /** | ||
486 | * Checks the current redo state. | ||
487 | * | ||
488 | * @returns {Boolean} Whether the document has a previous state to retrieve. | ||
489 | */ | ||
490 | redoable: function() { | ||
491 | return this.enabled && this.hasRedo; | ||
492 | }, | ||
493 | |||
494 | /** | ||
495 | * Checks the current undo state. | ||
496 | * | ||
497 | * @returns {Boolean} Whether the document has a future state to restore. | ||
498 | */ | ||
499 | undoable: function() { | ||
500 | return this.enabled && this.hasUndo; | ||
501 | }, | ||
502 | |||
503 | /** | ||
504 | * Performs an undo operation on current index. | ||
505 | */ | ||
506 | undo: function() { | ||
507 | if ( this.undoable() ) { | ||
508 | this.save( true ); | ||
509 | |||
510 | var image = this.getNextImage( true ); | ||
511 | if ( image ) | ||
512 | return this.restoreImage( image ), true; | ||
513 | } | ||
514 | |||
515 | return false; | ||
516 | }, | ||
517 | |||
518 | /** | ||
519 | * Performs a redo operation on current index. | ||
520 | */ | ||
521 | redo: function() { | ||
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. | ||
525 | this.save( true ); | ||
526 | |||
527 | // If instead we had changes, we can't redo anymore. | ||
528 | if ( this.redoable() ) { | ||
529 | var image = this.getNextImage( false ); | ||
530 | if ( image ) | ||
531 | return this.restoreImage( image ), true; | ||
532 | } | ||
533 | } | ||
534 | |||
535 | return false; | ||
536 | }, | ||
537 | |||
538 | /** | ||
539 | * Updates the last snapshot of the undo stack with the current editor content. | ||
540 | * | ||
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. | ||
543 | */ | ||
544 | update: function( newImage ) { | ||
545 | // Do not change snapshots stack is locked. | ||
546 | if ( this.locked ) | ||
547 | return; | ||
548 | |||
549 | if ( !newImage ) | ||
550 | newImage = new Image( this.editor ); | ||
551 | |||
552 | var i = this.index, | ||
553 | snapshots = this.snapshots; | ||
554 | |||
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 ] ) ) | ||
558 | i -= 1; | ||
559 | |||
560 | snapshots.splice( i, this.index - i + 1, newImage ); | ||
561 | this.index = i; | ||
562 | this.currentImage = newImage; | ||
563 | }, | ||
564 | |||
565 | /** | ||
566 | * Amends the last snapshot and changes its selection (only in case when content | ||
567 | * is equal between these two). | ||
568 | * | ||
569 | * @since 4.4.4 | ||
570 | * @param {CKEDITOR.plugins.undo.Image} newSnapshot New snapshot with new selection. | ||
571 | * @returns {Boolean} Returns `true` if selection was amended. | ||
572 | */ | ||
573 | updateSelection: function( newSnapshot ) { | ||
574 | if ( !this.snapshots.length ) | ||
575 | return false; | ||
576 | |||
577 | var snapshots = this.snapshots, | ||
578 | lastImage = snapshots[ snapshots.length - 1 ]; | ||
579 | |||
580 | if ( lastImage.equalsContent( newSnapshot ) ) { | ||
581 | if ( !lastImage.equalsSelection( newSnapshot ) ) { | ||
582 | snapshots[ snapshots.length - 1 ] = newSnapshot; | ||
583 | this.currentImage = newSnapshot; | ||
584 | return true; | ||
585 | } | ||
586 | } | ||
587 | |||
588 | return false; | ||
589 | }, | ||
590 | |||
591 | /** | ||
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. | ||
595 | * | ||
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. | ||
598 | * | ||
599 | * **Note:** For every `lock` call you must call {@link #unlock} once to unlock the undo manager. | ||
600 | * | ||
601 | * @since 4.0 | ||
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. | ||
613 | */ | ||
614 | lock: function( dontUpdate, forceUpdate ) { | ||
615 | if ( !this.locked ) { | ||
616 | if ( dontUpdate ) | ||
617 | this.locked = { level: 1 }; | ||
618 | else { | ||
619 | var update = null; | ||
620 | |||
621 | if ( forceUpdate ) | ||
622 | update = true; | ||
623 | else { | ||
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 ); | ||
630 | |||
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; | ||
636 | } | ||
637 | |||
638 | this.locked = { update: update, level: 1 }; | ||
639 | } | ||
640 | |||
641 | // Increase the level of lock. | ||
642 | } else { | ||
643 | this.locked.level++; | ||
644 | } | ||
645 | }, | ||
646 | |||
647 | /** | ||
648 | * Unlocks the snapshot stack and checks to amend the last snapshot. | ||
649 | * | ||
650 | * See {@link #lock} for more details. | ||
651 | * | ||
652 | * @since 4.0 | ||
653 | */ | ||
654 | unlock: function() { | ||
655 | if ( this.locked ) { | ||
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; | ||
659 | |||
660 | this.locked = null; | ||
661 | |||
662 | // forceUpdate was passed to lock(). | ||
663 | if ( update === true ) | ||
664 | this.update(); | ||
665 | // update is instance of Image. | ||
666 | else if ( update ) { | ||
667 | var newImage = new Image( this.editor, true ); | ||
668 | |||
669 | if ( !update.equalsContent( newImage ) ) | ||
670 | this.update(); | ||
671 | } | ||
672 | } | ||
673 | } | ||
674 | } | ||
675 | }; | ||
676 | |||
677 | /** | ||
678 | * Codes for navigation keys like *Arrows*, *Page Up/Down*, etc. | ||
679 | * Used by the {@link #isNavigationKey} method. | ||
680 | * | ||
681 | * @since 4.4.5 | ||
682 | * @readonly | ||
683 | * @static | ||
684 | */ | ||
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. | ||
689 | }; | ||
690 | |||
691 | /** | ||
692 | * Key groups identifier mapping. Used for accessing members in | ||
693 | * {@link #strokesRecorded}. | ||
694 | * | ||
695 | * * `FUNCTIONAL` – identifier for the *Backspace* / *Delete* key. | ||
696 | * * `PRINTABLE` – identifier for printable keys. | ||
697 | * | ||
698 | * Example usage: | ||
699 | * | ||
700 | * undoManager.strokesRecorded[ undoManager.keyGroups.FUNCTIONAL ]; | ||
701 | * | ||
702 | * @since 4.4.5 | ||
703 | * @readonly | ||
704 | * @static | ||
705 | */ | ||
706 | UndoManager.keyGroups = { | ||
707 | PRINTABLE: 0, | ||
708 | FUNCTIONAL: 1 | ||
709 | }; | ||
710 | |||
711 | /** | ||
712 | * Checks whether a key is one of navigation keys (*Arrows*, *Page Up/Down*, etc.). | ||
713 | * See also the {@link #navigationKeyCodes} property. | ||
714 | * | ||
715 | * @since 4.4.5 | ||
716 | * @static | ||
717 | * @param {Number} keyCode | ||
718 | * @returns {Boolean} | ||
719 | */ | ||
720 | UndoManager.isNavigationKey = function( keyCode ) { | ||
721 | return !!UndoManager.navigationKeyCodes[ keyCode ]; | ||
722 | }; | ||
723 | |||
724 | /** | ||
725 | * Returns the group to which the passed `keyCode` belongs. | ||
726 | * | ||
727 | * @since 4.4.5 | ||
728 | * @static | ||
729 | * @param {Number} keyCode | ||
730 | * @returns {Number} | ||
731 | */ | ||
732 | UndoManager.getKeyGroup = function( keyCode ) { | ||
733 | var keyGroups = UndoManager.keyGroups; | ||
734 | |||
735 | return backspaceOrDelete[ keyCode ] ? keyGroups.FUNCTIONAL : keyGroups.PRINTABLE; | ||
736 | }; | ||
737 | |||
738 | /** | ||
739 | * @since 4.4.5 | ||
740 | * @static | ||
741 | * @param {Number} keyGroup | ||
742 | * @returns {Number} | ||
743 | */ | ||
744 | UndoManager.getOppositeKeyGroup = function( keyGroup ) { | ||
745 | var keyGroups = UndoManager.keyGroups; | ||
746 | return ( keyGroup == keyGroups.FUNCTIONAL ? keyGroups.PRINTABLE : keyGroups.FUNCTIONAL ); | ||
747 | }; | ||
748 | |||
749 | /** | ||
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`. | ||
752 | * | ||
753 | * @since 4.4.5 | ||
754 | * @static | ||
755 | * @param {Number} keyCode | ||
756 | * @returns {Boolean} | ||
757 | */ | ||
758 | UndoManager.ieFunctionalKeysBug = function( keyCode ) { | ||
759 | return CKEDITOR.env.ie && UndoManager.getKeyGroup( keyCode ) == UndoManager.keyGroups.FUNCTIONAL; | ||
760 | }; | ||
761 | |||
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; | ||
766 | |||
767 | // Manually mark snapshot as available. | ||
768 | undoManager.hasUndo = true; | ||
769 | undoManager.hasRedo = false; | ||
770 | |||
771 | undoManager.onChange(); | ||
772 | } | ||
773 | |||
774 | /** | ||
775 | * Contains a snapshot of the editor content and selection at a given point in time. | ||
776 | * | ||
777 | * @private | ||
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. | ||
782 | */ | ||
783 | var Image = CKEDITOR.plugins.undo.Image = function( editor, contentsOnly ) { | ||
784 | this.editor = editor; | ||
785 | |||
786 | editor.fire( 'beforeUndoImage' ); | ||
787 | |||
788 | var contents = editor.getSnapshot(); | ||
789 | |||
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, '' ); | ||
793 | |||
794 | this.contents = contents; | ||
795 | |||
796 | if ( !contentsOnly ) { | ||
797 | var selection = contents && editor.getSelection(); | ||
798 | this.bookmarks = selection && selection.createBookmarks2( true ); | ||
799 | } | ||
800 | |||
801 | editor.fire( 'afterUndoImage' ); | ||
802 | }; | ||
803 | |||
804 | // Attributes that browser may changing them when setting via innerHTML. | ||
805 | var protectedAttrs = /\b(?:href|src|name)="[^"]*?"/gi; | ||
806 | |||
807 | Image.prototype = { | ||
808 | /** | ||
809 | * @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to. | ||
810 | * @returns {Boolean} Returns `true` if content in `otherImage` is the same. | ||
811 | */ | ||
812 | equalsContent: function( otherImage ) { | ||
813 | var thisContents = this.contents, | ||
814 | otherContents = otherImage.contents; | ||
815 | |||
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, '' ); | ||
820 | } | ||
821 | |||
822 | if ( thisContents != otherContents ) | ||
823 | return false; | ||
824 | |||
825 | return true; | ||
826 | }, | ||
827 | |||
828 | /** | ||
829 | * @param {CKEDITOR.plugins.undo.Image} otherImage Image to compare to. | ||
830 | * @returns {Boolean} Returns `true` if selection in `otherImage` is the same. | ||
831 | */ | ||
832 | equalsSelection: function( otherImage ) { | ||
833 | var bookmarksA = this.bookmarks, | ||
834 | bookmarksB = otherImage.bookmarks; | ||
835 | |||
836 | if ( bookmarksA || bookmarksB ) { | ||
837 | if ( !bookmarksA || !bookmarksB || bookmarksA.length != bookmarksB.length ) | ||
838 | return false; | ||
839 | |||
840 | for ( var i = 0; i < bookmarksA.length; i++ ) { | ||
841 | var bookmarkA = bookmarksA[ i ], | ||
842 | bookmarkB = bookmarksB[ i ]; | ||
843 | |||
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 ) ) { | ||
847 | return false; | ||
848 | } | ||
849 | } | ||
850 | } | ||
851 | |||
852 | return true; | ||
853 | } | ||
854 | |||
855 | /** | ||
856 | * Editor content. | ||
857 | * | ||
858 | * @readonly | ||
859 | * @property {String} contents | ||
860 | */ | ||
861 | |||
862 | /** | ||
863 | * Bookmarks representing the selection in an image. | ||
864 | * | ||
865 | * @readonly | ||
866 | * @property {Object[]} bookmarks Array of bookmark2 objects, see {@link CKEDITOR.dom.range#createBookmark2} for definition. | ||
867 | */ | ||
868 | }; | ||
869 | |||
870 | /** | ||
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). | ||
874 | * | ||
875 | * @since 4.4.4 | ||
876 | * @private | ||
877 | * @class CKEDITOR.plugins.undo.NativeEditingHandler | ||
878 | * @member CKEDITOR.plugins.undo Undo manager owning the handler. | ||
879 | * @constructor | ||
880 | * @param {CKEDITOR.plugins.undo.UndoManager} undoManager | ||
881 | */ | ||
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/ | ||
889 | |||
890 | /** | ||
891 | * An undo manager instance owning the editing handler. | ||
892 | * | ||
893 | * @property {CKEDITOR.plugins.undo.UndoManager} undoManager | ||
894 | */ | ||
895 | this.undoManager = undoManager; | ||
896 | |||
897 | /** | ||
898 | * See {@link #ignoreInputEventListener}. | ||
899 | * | ||
900 | * @since 4.4.5 | ||
901 | * @private | ||
902 | */ | ||
903 | this.ignoreInputEvent = false; | ||
904 | |||
905 | /** | ||
906 | * A stack of pressed keys. | ||
907 | * | ||
908 | * @since 4.4.5 | ||
909 | * @property {CKEDITOR.plugins.undo.KeyEventsStack} keyEventsStack | ||
910 | */ | ||
911 | this.keyEventsStack = new KeyEventsStack(); | ||
912 | |||
913 | /** | ||
914 | * An image of the editor during the `keydown` event (therefore without DOM modification). | ||
915 | * | ||
916 | * @property {CKEDITOR.plugins.undo.Image} lastKeydownImage | ||
917 | */ | ||
918 | this.lastKeydownImage = null; | ||
919 | }; | ||
920 | |||
921 | NativeEditingHandler.prototype = { | ||
922 | /** | ||
923 | * The `keydown` event listener. | ||
924 | * | ||
925 | * @param {CKEDITOR.dom.event} evt | ||
926 | */ | ||
927 | onKeydown: function( evt ) { | ||
928 | var keyCode = evt.data.getKey(); | ||
929 | |||
930 | // The composition is in progress - ignore the key. (#12597) | ||
931 | if ( keyCode === 229 ) { | ||
932 | return; | ||
933 | } | ||
934 | |||
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(); | ||
938 | return; | ||
939 | } | ||
940 | |||
941 | // Cleaning tab functional keys. | ||
942 | this.keyEventsStack.cleanUp( evt ); | ||
943 | |||
944 | var undoManager = this.undoManager; | ||
945 | |||
946 | // Gets last record for provided keyCode. If not found will create one. | ||
947 | var last = this.keyEventsStack.getLast( keyCode ); | ||
948 | if ( !last ) { | ||
949 | this.keyEventsStack.push( keyCode ); | ||
950 | } | ||
951 | |||
952 | // We need to store an image which will be used in case of key group | ||
953 | // change. | ||
954 | this.lastKeydownImage = new Image( undoManager.editor ); | ||
955 | |||
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. | ||
959 | |||
960 | // #12300 | ||
961 | undoManager.save( false, this.lastKeydownImage, false ); | ||
962 | undoManager.resetType(); | ||
963 | } | ||
964 | } | ||
965 | }, | ||
966 | |||
967 | /** | ||
968 | * The `input` event listener. | ||
969 | */ | ||
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; | ||
975 | return; | ||
976 | } | ||
977 | |||
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. | ||
981 | if ( !lastInput ) { | ||
982 | lastInput = this.keyEventsStack.push( 0 ); | ||
983 | } | ||
984 | |||
985 | // Increment inputs counter for provided key code. | ||
986 | this.keyEventsStack.increment( lastInput.keyCode ); | ||
987 | |||
988 | // Exceeded limit. | ||
989 | if ( this.keyEventsStack.getTotalInputs() >= this.undoManager.strokesLimit ) { | ||
990 | this.undoManager.type( lastInput.keyCode, true ); | ||
991 | this.keyEventsStack.resetInputs(); | ||
992 | } | ||
993 | }, | ||
994 | |||
995 | /** | ||
996 | * The `keyup` event listener. | ||
997 | * | ||
998 | * @param {CKEDITOR.dom.event} evt | ||
999 | */ | ||
1000 | onKeyup: function( evt ) { | ||
1001 | var undoManager = this.undoManager, | ||
1002 | keyCode = evt.data.getKey(), | ||
1003 | totalInputs = this.keyEventsStack.getTotalInputs(); | ||
1004 | |||
1005 | // Remove record from stack for provided key code. | ||
1006 | this.keyEventsStack.remove( keyCode ); | ||
1007 | |||
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 ) ) ) { | ||
1013 | return; | ||
1014 | } | ||
1015 | |||
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 ); | ||
1021 | } | ||
1022 | }, | ||
1023 | |||
1024 | /** | ||
1025 | * Method called for navigation change. At first it will check if current content does not differ | ||
1026 | * from the last saved snapshot. | ||
1027 | * | ||
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. | ||
1031 | * | ||
1032 | * @param {Boolean} skipContentCompare If set to `true`, it will not compare content, and only do a selection check. | ||
1033 | */ | ||
1034 | onNavigationKey: function( skipContentCompare ) { | ||
1035 | var undoManager = this.undoManager; | ||
1036 | |||
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 ) ); | ||
1041 | |||
1042 | undoManager.resetType(); | ||
1043 | }, | ||
1044 | |||
1045 | /** | ||
1046 | * Makes the next `input` event to be ignored. | ||
1047 | */ | ||
1048 | ignoreInputEventListener: function() { | ||
1049 | this.ignoreInputEvent = true; | ||
1050 | }, | ||
1051 | |||
1052 | /** | ||
1053 | * Attaches editable listeners required to provide the undo functionality. | ||
1054 | */ | ||
1055 | attachListeners: function() { | ||
1056 | var editor = this.undoManager.editor, | ||
1057 | editable = editor.editable(), | ||
1058 | that = this; | ||
1059 | |||
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 ); | ||
1064 | |||
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() ) ) { | ||
1068 | that.onInput(); | ||
1069 | } | ||
1070 | }, null, null, 999 ); | ||
1071 | |||
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 ); | ||
1074 | |||
1075 | // Keyup executes main snapshot logic. | ||
1076 | editable.attachListener( editable, 'keyup', that.onKeyup, that, null, 999 ); | ||
1077 | |||
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 ); | ||
1082 | |||
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. | ||
1086 | // #12324 comment:4 | ||
1087 | editable.attachListener( editable.isInline() ? editable : editor.document.getDocumentElement(), 'click', function() { | ||
1088 | that.onNavigationKey(); | ||
1089 | }, null, null, 999 ); | ||
1090 | |||
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 ); | ||
1097 | } | ||
1098 | }; | ||
1099 | |||
1100 | /** | ||
1101 | * This class represents a stack of pressed keys and stores information | ||
1102 | * about how many `input` events each key press has caused. | ||
1103 | * | ||
1104 | * @since 4.4.5 | ||
1105 | * @private | ||
1106 | * @class CKEDITOR.plugins.undo.KeyEventsStack | ||
1107 | * @constructor | ||
1108 | */ | ||
1109 | var KeyEventsStack = CKEDITOR.plugins.undo.KeyEventsStack = function() { | ||
1110 | /** | ||
1111 | * @readonly | ||
1112 | */ | ||
1113 | this.stack = []; | ||
1114 | }; | ||
1115 | |||
1116 | KeyEventsStack.prototype = { | ||
1117 | /** | ||
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. | ||
1120 | * | ||
1121 | * @param {Number} keyCode | ||
1122 | */ | ||
1123 | push: function( keyCode ) { | ||
1124 | var length = this.stack.push( { keyCode: keyCode, inputs: 0 } ); | ||
1125 | return this.stack[ length - 1 ]; | ||
1126 | }, | ||
1127 | |||
1128 | /** | ||
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`. | ||
1132 | * | ||
1133 | * @param {Number} [keyCode] | ||
1134 | * @returns {Number} | ||
1135 | */ | ||
1136 | getLastIndex: function( keyCode ) { | ||
1137 | if ( typeof keyCode != 'number' ) { | ||
1138 | return this.stack.length - 1; // Last index or -1. | ||
1139 | } else { | ||
1140 | var i = this.stack.length; | ||
1141 | while ( i-- ) { | ||
1142 | if ( this.stack[ i ].keyCode == keyCode ) { | ||
1143 | return i; | ||
1144 | } | ||
1145 | } | ||
1146 | return -1; | ||
1147 | } | ||
1148 | }, | ||
1149 | |||
1150 | /** | ||
1151 | * Returns the last key recorded in the stack. If `keyCode` is provided, then it will return | ||
1152 | * the last record for this `keyCode`. | ||
1153 | * | ||
1154 | * @param {Number} [keyCode] | ||
1155 | * @returns {Object} Last matching record or `null`. | ||
1156 | */ | ||
1157 | getLast: function( keyCode ) { | ||
1158 | var index = this.getLastIndex( keyCode ); | ||
1159 | if ( index != -1 ) { | ||
1160 | return this.stack[ index ]; | ||
1161 | } else { | ||
1162 | return null; | ||
1163 | } | ||
1164 | }, | ||
1165 | |||
1166 | /** | ||
1167 | * Increments registered input events for stack record for a given `keyCode`. | ||
1168 | * | ||
1169 | * @param {Number} keyCode | ||
1170 | */ | ||
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% | ||
1175 | } // %REMOVE_LINE% | ||
1176 | |||
1177 | found.inputs++; | ||
1178 | }, | ||
1179 | |||
1180 | /** | ||
1181 | * Removes the last record from the stack for the provided `keyCode`. | ||
1182 | * | ||
1183 | * @param {Number} keyCode | ||
1184 | */ | ||
1185 | remove: function( keyCode ) { | ||
1186 | var index = this.getLastIndex( keyCode ); | ||
1187 | |||
1188 | if ( index != -1 ) { | ||
1189 | this.stack.splice( index, 1 ); | ||
1190 | } | ||
1191 | }, | ||
1192 | |||
1193 | /** | ||
1194 | * Resets the `inputs` value to `0` for a given `keyCode` or in entire stack if a | ||
1195 | * `keyCode` is not specified. | ||
1196 | * | ||
1197 | * @param {Number} [keyCode] | ||
1198 | */ | ||
1199 | resetInputs: function( keyCode ) { | ||
1200 | if ( typeof keyCode == 'number' ) { | ||
1201 | var last = this.getLast( keyCode ); | ||
1202 | |||
1203 | if ( !last ) { // %REMOVE_LINE% | ||
1204 | throw new Error( 'Trying to reset inputs count, but could not found by keyCode: ' + keyCode + '.' ); // %REMOVE_LINE% | ||
1205 | } // %REMOVE_LINE% | ||
1206 | |||
1207 | last.inputs = 0; | ||
1208 | } else { | ||
1209 | var i = this.stack.length; | ||
1210 | while ( i-- ) { | ||
1211 | this.stack[ i ].inputs = 0; | ||
1212 | } | ||
1213 | } | ||
1214 | }, | ||
1215 | |||
1216 | /** | ||
1217 | * Sums up inputs number for each key code and returns it. | ||
1218 | * | ||
1219 | * @returns {Number} | ||
1220 | */ | ||
1221 | getTotalInputs: function() { | ||
1222 | var i = this.stack.length, | ||
1223 | total = 0; | ||
1224 | |||
1225 | while ( i-- ) { | ||
1226 | total += this.stack[ i ].inputs; | ||
1227 | } | ||
1228 | return total; | ||
1229 | }, | ||
1230 | |||
1231 | /** | ||
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. | ||
1236 | * | ||
1237 | * @param {CKEDITOR.dom.event} event | ||
1238 | */ | ||
1239 | cleanUp: function( event ) { | ||
1240 | var nativeEvent = event.data.$; | ||
1241 | |||
1242 | if ( !( nativeEvent.ctrlKey || nativeEvent.metaKey ) ) { | ||
1243 | this.remove( 17 ); | ||
1244 | } | ||
1245 | if ( !nativeEvent.shiftKey ) { | ||
1246 | this.remove( 16 ); | ||
1247 | } | ||
1248 | if ( !nativeEvent.altKey ) { | ||
1249 | this.remove( 18 ); | ||
1250 | } | ||
1251 | } | ||
1252 | }; | ||
1253 | } )(); | ||
1254 | |||
1255 | /** | ||
1256 | * The number of undo steps to be saved. The higher value is set, the more | ||
1257 | * memory is used for it. | ||
1258 | * | ||
1259 | * config.undoStackSize = 50; | ||
1260 | * | ||
1261 | * @cfg {Number} [undoStackSize=20] | ||
1262 | * @member CKEDITOR.config | ||
1263 | */ | ||
1264 | |||
1265 | /** | ||
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. | ||
1268 | * | ||
1269 | * @event saveSnapshot | ||
1270 | * @member CKEDITOR.editor | ||
1271 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1272 | */ | ||
1273 | |||
1274 | /** | ||
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. | ||
1278 | * | ||
1279 | * @since 3.5.3 | ||
1280 | * @event beforeUndoImage | ||
1281 | * @member CKEDITOR.editor | ||
1282 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1283 | * @see CKEDITOR.editor#afterUndoImage | ||
1284 | */ | ||
1285 | |||
1286 | /** | ||
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. | ||
1290 | * | ||
1291 | * @since 3.5.3 | ||
1292 | * @event afterUndoImage | ||
1293 | * @member CKEDITOR.editor | ||
1294 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1295 | * @see CKEDITOR.editor#beforeUndoImage | ||
1296 | */ | ||
1297 | |||
1298 | /** | ||
1299 | * Fired when the content of the editor is changed. | ||
1300 | * | ||
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. | ||
1305 | * | ||
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. | ||
1309 | * | ||
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). | ||
1314 | * | ||
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. | ||
1320 | * } ); | ||
1321 | * } | ||
1322 | * } ); | ||
1323 | * | ||
1324 | * @since 4.2 | ||
1325 | * @event change | ||
1326 | * @member CKEDITOR.editor | ||
1327 | * @param {CKEDITOR.editor} editor This editor instance. | ||
1328 | */ | ||