undo.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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 'ckeditor5-core/src/plugin';
  9. import UndoEngine from './undoengine';
  10. import ButtonView from '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 with selection before A1]
  30. * [delta B1] [batch B with selection before B1]
  31. * [delta B2] [batch C with selection before C1]
  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 with selection before A1]
  51. * [delta B1 ] [batch B with selection before B1]
  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". As can be seen, `C2r` is transformed by a delta which is undone afterwards anyway.
  62. * This brings two problems: lower effectiveness (obvious) and incorrect results. Bad results come from the fact that
  63. * operational transformation algorithms assume there is no connection between two transformed operations when resolving
  64. * conflicts, which is true for example for collaborative editing, but is not true for the undo algorithm.
  65. *
  66. * To prevent both problems, `History` introduces an API to {@link module:engine/model/history~History#removeDelta remove}
  67. * deltas from history. It is used to remove undone and undoing deltas after they are applied. It feels right — since when a
  68. * delta is undone or reversed, it is "removed" and there should be no sign of it in the history (fig. 1).
  69. *
  70. * Notes:
  71. *
  72. * * `---` symbolizes a removed delta.
  73. * * `'` symbolizes a reversed delta that was later transformed.
  74. *
  75. * History (fig. 1) History (fig. 2) History (fig. 3)
  76. * ================ ================ ================
  77. * [delta A1] [delta A1] [delta A1]
  78. * [delta B1] [delta B1] [delta B1]
  79. * [delta B2] [delta B2] [delta B2]
  80. * [delta C1] [delta C1] [---C1---]
  81. * [delta C2] [---C2---] [---C2---]
  82. * [delta B3] [delta B3] [delta B3]
  83. * [---C3---] [---C3---] [---C3---]
  84. * [---C3r--] [---C3r--] [---C3r--]
  85. * [---C2'--] [---C2'--]
  86. * [---C1'--]
  87. *
  88. * `C2r` can now be transformed only by `B3` and both `C2'` and `C2` can be removed (fig. 2). Same with `C1` (fig. 3).
  89. *
  90. * But what about that selection? For batch `C`, undo feature remembers the selection just before `C1` was applied. It can be
  91. * visualized between delta `B2` and `B3` (see fig. 3). As can be seen, some operations were applied to the document since the selection
  92. * state was remembered. Setting the document selection as it was remembered would be incorrect. It feels natural that
  93. * the selection state should also be transformed by deltas from history. The same pattern applies as with transforming deltas —
  94. * ranges should not be transformed by undone and undoing deltas. Thankfully, those deltas are already removed from history.
  95. *
  96. * Unfortunately, a problem appears with delta `B3`. It still remembers the context of deltas `C2` and `C1` on which it bases.
  97. * It is an obvious error — transforming by that delta would lead to incorrect results or "repeating" history would
  98. * produce a different document than the actual one.
  99. *
  100. * To prevent this situation, `B3` needs to also be {@link module:engine/model/history~History#updateDelta updated} in history.
  101. * It should be kept in a state that "does not remember" deltas that were removed from history. It is easily
  102. * achieved while transforming the reversed delta. For example, when `C2r` is transformed by `B3`, at the same time `B3` is
  103. * transformed by `C2r`. Transforming `B3` that remembers `C2` by a delta reversing `C2` effectively makes `B3` "forget" about `C2`.
  104. * By doing these transformations you effectively make `B3` base on `B2` which is the correct state of history (fig. 4).
  105. *
  106. * History (fig. 4) History (fig. 5)
  107. * =========================== ===============================
  108. * [delta A1] [---A1---]
  109. * [delta B1] [delta B1 "without A1"]
  110. * [delta B2] [delta B2 "without A1"]
  111. * [---C1---] [---C1---]
  112. * [---C2---] [---C2---]
  113. * [delta B3 "without C2, C1"] [delta B3 "without C2, C1, A1"]
  114. * [---C3---] [---C3---]
  115. * [---C3r--] [---C3r--]
  116. * [---C2'--] [---C2'--]
  117. * [---C1'--] [---C1'--]
  118. * [---A1'--]
  119. *
  120. * Selective undo works on the same basis, however, instead of undoing the last batch in the undo stack, any batch can be undone.
  121. * The same algorithm applies: deltas from a batch (i.e. `A1`) are reversed and then transformed by deltas stored in history,
  122. * simultaneously updating them. Then deltas are applied to the document and removed from history (fig. 5).
  123. *
  124. * @extends module:core/plugin~Plugin
  125. */
  126. export default class Undo extends Plugin {
  127. /**
  128. * @inheritDoc
  129. */
  130. static get requires() {
  131. return [ UndoEngine ];
  132. }
  133. /**
  134. * @inheritDoc
  135. */
  136. init() {
  137. const editor = this.editor;
  138. const t = editor.t;
  139. this._addButton( 'undo', t( 'Undo' ), 'CTRL+Z', undoIcon );
  140. this._addButton( 'redo', t( 'Redo' ), 'CTRL+Y', redoIcon );
  141. editor.keystrokes.set( 'CTRL+Z', 'undo' );
  142. editor.keystrokes.set( 'CTRL+Y', 'redo' );
  143. editor.keystrokes.set( 'CTRL+SHIFT+Z', 'redo' );
  144. }
  145. /**
  146. * Creates a button for the specified command.
  147. *
  148. * @private
  149. * @param {String} name Command name.
  150. * @param {String} label Button label.
  151. * @param {String} keystroke Command keystroke.
  152. * @param {String} Icon Source of the icon.
  153. */
  154. _addButton( name, label, keystroke, Icon ) {
  155. const editor = this.editor;
  156. const command = editor.commands.get( name );
  157. editor.ui.componentFactory.add( name, ( locale ) => {
  158. const view = new ButtonView( locale );
  159. view.set( {
  160. label: label,
  161. icon: Icon,
  162. keystroke,
  163. tooltip: true
  164. } );
  165. view.bind( 'isEnabled' ).to( command, 'isEnabled' );
  166. this.listenTo( view, 'execute', () => editor.execute( name ) );
  167. return view;
  168. } );
  169. }
  170. }