4 * Copyright, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://www.tinymce.com/license
8 * Contributing: http://www.tinymce.com/contributing
12 * 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.
14 * @class tinymce.UndoManager
16 define("tinymce/UndoManager", [
19 ], function(Env, Tools) {
20 var trim = Tools.trim, trimContentRegExp;
22 trimContentRegExp = new RegExp([
23 '<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers
24 '<div[^>]+data-mce-bogus[^>]+><\\/div>', // Trim bogus divs like resize handles
25 '\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected
28 return function(editor) {
29 var self, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, lock;
31 // Returns a trimmed version of the current editor contents
32 function getContent() {
33 return trim(editor.getContent({format: 'raw', no_events: 1}).replace(trimContentRegExp, ''));
36 function addNonTypingUndoLevel() {
41 // Add initial undo level when the editor is initialized
42 editor.on('init', function() {
46 // Get position before an execCommand is processed
47 editor.on('BeforeExecCommand', function(e) {
50 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') {
55 // Add undo level after an execCommand call was made
56 editor.on('ExecCommand', function(e) {
59 if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') {
64 editor.on('ObjectResizeStart', function() {
68 editor.on('SaveContent ObjectResized', addNonTypingUndoLevel);
69 editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel);
70 editor.dom.bind(editor.getBody(), 'focusout', function() {
71 if (!editor.removed && self.typing) {
72 addNonTypingUndoLevel();
76 editor.on('KeyUp', function(e) {
77 var keyCode = e.keyCode;
79 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) {
80 addNonTypingUndoLevel();
84 if (keyCode == 46 || keyCode == 8 || (Env.mac && (keyCode == 91 || keyCode == 93))) {
88 // Fire a TypingUndo event on the first character entered
89 if (isFirstTypedCharacter && self.typing) {
90 // Make the it dirty if the content was changed after typing the first character
91 if (!editor.isDirty()) {
92 editor.isNotDirty = !data[0] || getContent() == data[0].content;
94 // Fire initial change event
95 if (!editor.isNotDirty) {
96 editor.fire('change', {level: data[0], lastLevel: null});
100 editor.fire('TypingUndo');
101 isFirstTypedCharacter = false;
102 editor.nodeChanged();
106 editor.on('KeyDown', function(e) {
107 var keyCode = e.keyCode;
109 // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter
110 if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) {
112 addNonTypingUndoLevel();
118 // If key isn't shift,ctrl,alt,capslock,metakey
119 if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) {
123 isFirstTypedCharacter = true;
127 editor.on('MouseDown', function() {
129 addNonTypingUndoLevel();
133 // Add keyboard shortcuts for undo/redo keys
134 editor.addShortcut('ctrl+z', '', 'Undo');
135 editor.addShortcut('ctrl+y,ctrl+shift+z', '', 'Redo');
137 editor.on('AddUndo Undo Redo ClearUndos MouseUp', function(e) {
138 if (!e.isDefaultPrevented()) {
139 editor.nodeChanged();
144 // Explose for debugging reasons
148 * State if the user is currently typing or not. This will add a typing operation into one undo
149 * level instead of one new level for each keystroke.
151 * @field {Boolean} typing
156 * Stores away a bookmark to be used when performing an undo action so that the selection is before
157 * the change has been made.
159 * @method beforeChange
161 beforeChange: function() {
163 beforeBookmark = editor.selection.getBookmark(2, true);
168 * Adds a new undo level/snapshot to the undo list.
171 * @param {Object} l Optional undo level object to add.
172 * @return {Object} Undo level that got added or null it a level wasn't needed.
174 add: function(level) {
175 var i, settings = editor.settings, lastLevel;
178 level.content = getContent();
180 if (lock || editor.fire('BeforeAddUndo', {level: level}).isDefaultPrevented()) {
184 // Add undo level if needed
185 lastLevel = data[index];
186 if (lastLevel && lastLevel.content == level.content) {
190 // Set before bookmark on previous level
192 data[index].beforeBookmark = beforeBookmark;
196 if (settings.custom_undo_redo_levels) {
197 if (data.length > settings.custom_undo_redo_levels) {
198 for (i = 0; i < data.length - 1; i++) {
199 data[i] = data[i + 1];
207 // Get a non intrusive normalized bookmark
208 level.bookmark = editor.selection.getBookmark(2, true);
210 // Crop array if needed
211 if (index < data.length - 1) {
212 data.length = index + 1;
216 index = data.length - 1;
218 var args = {level: level, lastLevel: lastLevel};
220 editor.fire('AddUndo', args);
223 editor.fire('change', args);
224 editor.isNotDirty = false;
231 * Undoes the last action.
234 * @return {Object} Undo level or null if no undo was performed.
245 level = data[--index];
247 editor.setContent(level.content, {format: 'raw'});
248 editor.selection.moveToBookmark(level.beforeBookmark);
250 editor.fire('undo', {level: level});
257 * Redoes the last action.
260 * @return {Object} Redo level or null if no redo was performed.
265 if (index < data.length - 1) {
266 level = data[++index];
268 editor.setContent(level.content, {format: 'raw'});
269 editor.selection.moveToBookmark(level.bookmark);
271 editor.fire('redo', {level: level});
278 * Removes all undo levels.
286 editor.fire('ClearUndos');
290 * Returns true/false if the undo manager has any undo levels.
293 * @return {Boolean} true/false if the undo manager has any undo levels.
295 hasUndo: function() {
296 // Has undo levels or typing and content isn't the same as the initial level
297 return index > 0 || (self.typing && data[0] && getContent() != data[0].content);
301 * Returns true/false if the undo manager has any redo levels.
304 * @return {Boolean} true/false if the undo manager has any redo levels.
306 hasRedo: function() {
307 return index < data.length - 1 && !this.typing;
311 * Executes the specified function in an undo transation. The selection
312 * before the modification will be stored to the undo stack and if the DOM changes
313 * it will add a new undo level. Any methods within the transation that adds undo levels will
314 * be ignored. So a transation can include calls to execCommand or editor.insertContent.
317 * @param {function} callback Function to execute dom manipulation logic in.
319 transact: function(callback) {