]>
Commit | Line | Data |
---|---|---|
7adcb81e IB |
1 | /**\r |
2 | * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.\r | |
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 | |
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%\r | |
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` – identifier for the *Backspace* / *Delete* key.\r | |
696 | * * `PRINTABLE` – 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 |