plugin.js 41 KB

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