UndoManager.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /**
  2. * UndoManager.js
  3. *
  4. * Copyright, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://www.tinymce.com/license
  8. * Contributing: http://www.tinymce.com/contributing
  9. */
  10. /**
  11. * This class handles the undo/redo history levels for the editor. Since the build in undo/redo has major drawbacks a custom one was needed.
  12. *
  13. * @class tinymce.UndoManager
  14. */
  15. define("tinymce/UndoManager", [
  16. "tinymce/Env",
  17. "tinymce/util/Tools"
  18. ], function(Env, Tools) {
  19. var trim = Tools.trim, trimContentRegExp;
  20. trimContentRegExp = new RegExp([
  21. '<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers
  22. '<div[^>]+data-mce-bogus[^>]+><\\/div>', // Trim bogus divs like resize handles
  23. '\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected
  24. ].join('|'), 'gi');
  25. return function(editor) {
  26. var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0;
  27. // Returns a trimmed version of the current editor contents
  28. function getContent() {
  29. return trim(editor.getContent({format: 'raw', no_events: 1}).replace(trimContentRegExp, ''));
  30. }
  31. function addNonTypingUndoLevel(e) {
  32. self.typing = false;
  33. self.add({}, e);
  34. }
  35. // Add initial undo level when the editor is initialized
  36. editor.on('init', function() {
  37. self.add();
  38. });
  39. // Get position before an execCommand is processed
  40. editor.on('BeforeExecCommand', function(e) {
  41. var cmd = e.command;
  42. if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') {
  43. self.beforeChange();
  44. }
  45. });
  46. // Add undo level after an execCommand call was made
  47. editor.on('ExecCommand', function(e) {
  48. var cmd = e.command;
  49. if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') {
  50. addNonTypingUndoLevel(e);
  51. }
  52. });
  53. editor.on('ObjectResizeStart', function() {
  54. self.beforeChange();
  55. });
  56. editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel);
  57. editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel);
  58. editor.on('KeyUp', function(e) {
  59. var keyCode = e.keyCode;
  60. if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) {
  61. addNonTypingUndoLevel();
  62. editor.nodeChanged();
  63. }
  64. if (keyCode == 46 || keyCode == 8 || (Env.mac && (keyCode == 91 || keyCode == 93))) {
  65. editor.nodeChanged();
  66. }
  67. // Fire a TypingUndo event on the first character entered
  68. if (isFirstTypedCharacter && self.typing) {
  69. // Make the it dirty if the content was changed after typing the first character
  70. if (!editor.isDirty()) {
  71. editor.isNotDirty = !data[0] || getContent() == data[0].content;
  72. // Fire initial change event
  73. if (!editor.isNotDirty) {
  74. editor.fire('change', {level: data[0], lastLevel: null});
  75. }
  76. }
  77. editor.fire('TypingUndo');
  78. isFirstTypedCharacter = false;
  79. editor.nodeChanged();
  80. }
  81. });
  82. editor.on('KeyDown', function(e) {
  83. var keyCode = e.keyCode;
  84. // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter
  85. if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) {
  86. if (self.typing) {
  87. addNonTypingUndoLevel(e);
  88. }
  89. return;
  90. }
  91. // If key isn't shift,ctrl,alt,capslock,metakey
  92. if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) {
  93. self.beforeChange();
  94. self.typing = true;
  95. self.add({}, e);
  96. isFirstTypedCharacter = true;
  97. }
  98. });
  99. editor.on('MouseDown', function(e) {
  100. if (self.typing) {
  101. addNonTypingUndoLevel(e);
  102. }
  103. });
  104. // Add keyboard shortcuts for undo/redo keys
  105. editor.addShortcut('ctrl+z', '', 'Undo');
  106. editor.addShortcut('ctrl+y,ctrl+shift+z', '', 'Redo');
  107. editor.on('AddUndo Undo Redo ClearUndos MouseUp', function(e) {
  108. if (!e.isDefaultPrevented()) {
  109. editor.nodeChanged();
  110. }
  111. });
  112. self = {
  113. // Explose for debugging reasons
  114. data: data,
  115. /**
  116. * State if the user is currently typing or not. This will add a typing operation into one undo
  117. * level instead of one new level for each keystroke.
  118. *
  119. * @field {Boolean} typing
  120. */
  121. typing: false,
  122. /**
  123. * Stores away a bookmark to be used when performing an undo action so that the selection is before
  124. * the change has been made.
  125. *
  126. * @method beforeChange
  127. */
  128. beforeChange: function() {
  129. if (!locks) {
  130. beforeBookmark = editor.selection.getBookmark(2, true);
  131. }
  132. },
  133. /**
  134. * Adds a new undo level/snapshot to the undo list.
  135. *
  136. * @method add
  137. * @param {Object} level Optional undo level object to add.
  138. * @param {DOMEvent} Event Optional event responsible for the creation of the undo level.
  139. * @return {Object} Undo level that got added or null it a level wasn't needed.
  140. */
  141. add: function(level, event) {
  142. var i, settings = editor.settings, lastLevel;
  143. level = level || {};
  144. level.content = getContent();
  145. if (locks || editor.removed) {
  146. return null;
  147. }
  148. lastLevel = data[index];
  149. if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) {
  150. return null;
  151. }
  152. // Add undo level if needed
  153. if (lastLevel && lastLevel.content == level.content) {
  154. return null;
  155. }
  156. // Set before bookmark on previous level
  157. if (data[index]) {
  158. data[index].beforeBookmark = beforeBookmark;
  159. }
  160. // Time to compress
  161. if (settings.custom_undo_redo_levels) {
  162. if (data.length > settings.custom_undo_redo_levels) {
  163. for (i = 0; i < data.length - 1; i++) {
  164. data[i] = data[i + 1];
  165. }
  166. data.length--;
  167. index = data.length;
  168. }
  169. }
  170. // Get a non intrusive normalized bookmark
  171. level.bookmark = editor.selection.getBookmark(2, true);
  172. // Crop array if needed
  173. if (index < data.length - 1) {
  174. data.length = index + 1;
  175. }
  176. data.push(level);
  177. index = data.length - 1;
  178. var args = {level: level, lastLevel: lastLevel, originalEvent: event};
  179. editor.fire('AddUndo', args);
  180. if (index > 0) {
  181. editor.isNotDirty = false;
  182. editor.fire('change', args);
  183. }
  184. return level;
  185. },
  186. /**
  187. * Undoes the last action.
  188. *
  189. * @method undo
  190. * @return {Object} Undo level or null if no undo was performed.
  191. */
  192. undo: function() {
  193. var level;
  194. if (self.typing) {
  195. self.add();
  196. self.typing = false;
  197. }
  198. if (index > 0) {
  199. level = data[--index];
  200. // Undo to first index then set dirty state to false
  201. if (index === 0) {
  202. editor.isNotDirty = true;
  203. }
  204. editor.setContent(level.content, {format: 'raw'});
  205. editor.selection.moveToBookmark(level.beforeBookmark);
  206. editor.fire('undo', {level: level});
  207. }
  208. return level;
  209. },
  210. /**
  211. * Redoes the last action.
  212. *
  213. * @method redo
  214. * @return {Object} Redo level or null if no redo was performed.
  215. */
  216. redo: function() {
  217. var level;
  218. if (index < data.length - 1) {
  219. level = data[++index];
  220. editor.setContent(level.content, {format: 'raw'});
  221. editor.selection.moveToBookmark(level.bookmark);
  222. editor.fire('redo', {level: level});
  223. }
  224. return level;
  225. },
  226. /**
  227. * Removes all undo levels.
  228. *
  229. * @method clear
  230. */
  231. clear: function() {
  232. data = [];
  233. index = 0;
  234. self.typing = false;
  235. editor.fire('ClearUndos');
  236. },
  237. /**
  238. * Returns true/false if the undo manager has any undo levels.
  239. *
  240. * @method hasUndo
  241. * @return {Boolean} true/false if the undo manager has any undo levels.
  242. */
  243. hasUndo: function() {
  244. // Has undo levels or typing and content isn't the same as the initial level
  245. return index > 0 || (self.typing && data[0] && getContent() != data[0].content);
  246. },
  247. /**
  248. * Returns true/false if the undo manager has any redo levels.
  249. *
  250. * @method hasRedo
  251. * @return {Boolean} true/false if the undo manager has any redo levels.
  252. */
  253. hasRedo: function() {
  254. return index < data.length - 1 && !this.typing;
  255. },
  256. /**
  257. * Executes the specified function in an undo transation. The selection
  258. * before the modification will be stored to the undo stack and if the DOM changes
  259. * it will add a new undo level. Any methods within the transation that adds undo levels will
  260. * be ignored. So a transation can include calls to execCommand or editor.insertContent.
  261. *
  262. * @method transact
  263. * @param {function} callback Function to execute dom manipulation logic in.
  264. */
  265. transact: function(callback) {
  266. self.beforeChange();
  267. try {
  268. locks++;
  269. callback();
  270. } finally {
  271. locks--;
  272. }
  273. self.add();
  274. }
  275. };
  276. return self;
  277. };
  278. });