undo.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /**
  2. * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
  3. * For licensing, see LICENSE.md.
  4. */
  5. /**
  6. * @module undo/undo
  7. */
  8. import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
  9. import UndoEngine from './undoengine';
  10. import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
  11. import undoIcon from '../theme/icons/undo.svg';
  12. import redoIcon from '../theme/icons/redo.svg';
  13. /**
  14. * The undo feature. It introduces the Undo and Redo buttons to the editor.
  15. *
  16. * Below is the explanation of the undo mechanism working together with {@link module:engine/model/history~History History}:
  17. *
  18. * Whenever a {@link module:engine/model/delta/delta~Delta delta} is applied to the
  19. * {@link module:engine/model/document~Document document}, it is saved to `History` as is.
  20. * The {@link module:engine/model/batch~Batch batch} that owns that delta is also saved, in
  21. * {@link module:undo/undocommand~UndoCommand}, together with the selection that was present in the document before the
  22. * delta was applied. A batch is saved instead of the delta because changes are undone batch-by-batch, not delta-by-delta
  23. * and a batch is seen as one undo step.
  24. *
  25. * After some changes happen to the document, the `History` and `UndoCommand` stack can be represented as follows:
  26. *
  27. * History Undo stack
  28. * =========== ==================================
  29. * [delta A1] [batch A]
  30. * [delta B1] [batch B]
  31. * [delta B2] [batch C]
  32. * [delta C1]
  33. * [delta C2]
  34. * [delta B3]
  35. * [delta C3]
  36. *
  37. * Where deltas starting with the same letter are from same batch.
  38. *
  39. * Undoing a batch means that a set of deltas which will reverse the effects of that batch needs to be generated. For example, if a batch
  40. * added several letters, undoing the batch should remove them. It is important to apply undoing deltas in the reversed order,
  41. * so if a batch has delta `X`, `Y`, `Z`, reversed deltas `Zr`, `Yr` and `Xr` need to be applied. Otherwise reversed delta
  42. * `Xr` would operate on a wrong document state, because delta `X` does not know that deltas `Y` and `Z` happened.
  43. *
  44. * After deltas from an undone batch got {@link module:engine/model/delta/delta~Delta#getReversed reversed},
  45. * one needs to make sure if they are ready to be applied. In the scenario above, delta `C3` is the last delta and `C3r`
  46. * bases on up-to-date document state, so it can be applied to the document.
  47. *
  48. * History Undo stack
  49. * ============= ==================================
  50. * [ delta A1 ] [ batch A ]
  51. * [ delta B1 ] [ batch B ]
  52. * [ delta B2 ] [ processing undoing batch C ]
  53. * [ delta C1 ]
  54. * [ delta C2 ]
  55. * [ delta B3 ]
  56. * [ delta C3 ]
  57. * [ delta C3r ]
  58. *
  59. * Next is delta `C2`, reversed to `C2r`. `C2r` bases on `C2`, so it bases on the wrong document state. It needs to be
  60. * transformed by deltas from history that happened after it, so it "knows" about them. Let us assume that `C2' = C2r * B3 * C3 * C3r`,
  61. * where `*` means "transformed by". Rest of deltas from that batch are processed in the same fashion.
  62. *
  63. * History Undo stack Redo stack
  64. * ============= ================================== ==================================
  65. * [ delta A1 ] [ batch A ] [ batch Cr ]
  66. * [ delta B1 ] [ batch B ]
  67. * [ delta B2 ]
  68. * [ delta C1 ]
  69. * [ delta C2 ]
  70. * [ delta B3 ]
  71. * [ delta C3 ]
  72. * [ delta C3r ]
  73. * [ delta C2' ]
  74. * [ delta C1' ]
  75. *
  76. * Selective undo works on the same basis, however, instead of undoing the last batch in the undo stack, any batch can be undone.
  77. * The same algorithm applies: deltas from a batch (i.e. `A1`) are reversed and then transformed by deltas stored in history.
  78. *
  79. * Redo also is very similar to undo. It has its own stack that is filled with undoing (reversed batches). Deltas from
  80. * batch that is re-done are reversed-back, transformed in proper order and applied to the document.
  81. *
  82. * History Undo stack Redo stack
  83. * ============= ================================== ==================================
  84. * [ delta A1 ] [ batch A ]
  85. * [ delta B1 ] [ batch B ]
  86. * [ delta B2 ] [ batch Crr ]
  87. * [ delta C1 ]
  88. * [ delta C2 ]
  89. * [ delta B3 ]
  90. * [ delta C3 ]
  91. * [ delta C3r ]
  92. * [ delta C2' ]
  93. * [ delta C1' ]
  94. * [ delta C1'r]
  95. * [ delta C2'r]
  96. * [ delta C3rr]
  97. *
  98. * @extends module:core/plugin~Plugin
  99. */
  100. export default class Undo extends Plugin {
  101. /**
  102. * @inheritDoc
  103. */
  104. static get requires() {
  105. return [ UndoEngine ];
  106. }
  107. /**
  108. * @inheritDoc
  109. */
  110. static get pluginName() {
  111. return 'Undo';
  112. }
  113. /**
  114. * @inheritDoc
  115. */
  116. init() {
  117. const editor = this.editor;
  118. const t = editor.t;
  119. this._addButton( 'undo', t( 'Undo' ), 'CTRL+Z', undoIcon );
  120. this._addButton( 'redo', t( 'Redo' ), 'CTRL+Y', redoIcon );
  121. editor.keystrokes.set( 'CTRL+Z', 'undo' );
  122. editor.keystrokes.set( 'CTRL+Y', 'redo' );
  123. editor.keystrokes.set( 'CTRL+SHIFT+Z', 'redo' );
  124. }
  125. /**
  126. * Creates a button for the specified command.
  127. *
  128. * @private
  129. * @param {String} name Command name.
  130. * @param {String} label Button label.
  131. * @param {String} keystroke Command keystroke.
  132. * @param {String} Icon Source of the icon.
  133. */
  134. _addButton( name, label, keystroke, Icon ) {
  135. const editor = this.editor;
  136. const command = editor.commands.get( name );
  137. editor.ui.componentFactory.add( name, locale => {
  138. const view = new ButtonView( locale );
  139. view.set( {
  140. label,
  141. icon: Icon,
  142. keystroke,
  143. tooltip: true
  144. } );
  145. view.bind( 'isEnabled' ).to( command, 'isEnabled' );
  146. this.listenTo( view, 'execute', () => editor.execute( name ) );
  147. return view;
  148. } );
  149. }
  150. }