1. (function(tinymce) {
  2.     var Dispatcher = tinymce.util.Dispatcher;
  3.  
  4.     tinymce.UndoManager = function(editor) {
  5.         var self, index = 0, data = [], beforeBookmark, onAdd, onUndo, onRedo;
  6.  
  7.         function getContent() {
  8.             // Remove whitespace before/after and remove pure bogus nodes
  9.             return tinymce.trim(editor.getContent({format : 'raw', no_events : 1}).replace(/<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\/span>/g, ''));
  10.         };
  11.  
  12.         function addNonTypingUndoLevel() {
  13.             self.typing = false;
  14.             self.add();
  15.         };
  16.  
  17.         // Create event instances
  18.         onBeforeAdd = new Dispatcher(self);
  19.         onAdd       = new Dispatcher(self);
  20.         onUndo      = new Dispatcher(self);
  21.         onRedo      = new Dispatcher(self);
  22.  
  23.         // Pass though onAdd event from UndoManager to Editor as onChange
  24.         onAdd.add(function(undoman, level) {
  25.             if (undoman.hasUndo())
  26.                 return editor.onChange.dispatch(editor, level, undoman);
  27.         });
  28.  
  29.         // Pass though onUndo event from UndoManager to Editor
  30.         onUndo.add(function(undoman, level) {
  31.             return editor.onUndo.dispatch(editor, level, undoman);
  32.         });
  33.  
  34.         // Pass though onRedo event from UndoManager to Editor
  35.         onRedo.add(function(undoman, level) {
  36.             return editor.onRedo.dispatch(editor, level, undoman);
  37.         });
  38.  
  39.         // Add initial undo level when the editor is initialized
  40.         editor.onInit.add(function() {
  41.             self.add();
  42.         });
  43.  
  44.         // Get position before an execCommand is processed
  45.         editor.onBeforeExecCommand.add(function(ed, cmd, ui, val, args) {
  46.             if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) {
  47.                 self.beforeChange();
  48.             }
  49.         });
  50.  
  51.         // Add undo level after an execCommand call was made
  52.         editor.onExecCommand.add(function(ed, cmd, ui, val, args) {
  53.             if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint' && (!args || !args.skip_undo)) {
  54.                 self.add();
  55.             }
  56.         });
  57.  
  58.         // Add undo level on save contents, drag end and blur/focusout
  59.         editor.onSaveContent.add(addNonTypingUndoLevel);
  60.         editor.dom.bind(editor.dom.getRoot(), 'dragend', addNonTypingUndoLevel);
  61.         editor.dom.bind(editor.getBody(), 'focusout', function(e) {
  62.             if (!editor.removed && self.typing) {
  63.                 addNonTypingUndoLevel();
  64.             }
  65.         });
  66.  
  67.         editor.onKeyUp.add(function(editor, e) {
  68.             var keyCode = e.keyCode;
  69.  
  70.             if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) {
  71.                 addNonTypingUndoLevel();
  72.             }
  73.         });
  74.  
  75.         editor.onKeyDown.add(function(editor, e) {
  76.             var keyCode = e.keyCode;
  77.  
  78.             // Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter
  79.             if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) {
  80.                 if (self.typing) {
  81.                     addNonTypingUndoLevel();
  82.                 }
  83.  
  84.                 return;
  85.             }
  86.  
  87.             // If key isn't shift,ctrl,alt,capslock,metakey
  88.             if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing) {
  89.                 self.beforeChange();
  90.                 self.typing = true;
  91.                 self.add();
  92.             }
  93.         });
  94.  
  95.         editor.onMouseDown.add(function(editor, e) {
  96.             if (self.typing) {
  97.                 addNonTypingUndoLevel();
  98.             }
  99.         });
  100.  
  101.         // Add keyboard shortcuts for undo/redo keys
  102.         editor.addShortcut('ctrl+z', 'undo_desc', 'Undo');
  103.         editor.addShortcut('ctrl+y', 'redo_desc', 'Redo');
  104.  
  105.         self = {
  106.             // Explose for debugging reasons
  107.             data : data,
  108.  
  109.             typing : false,
  110.            
  111.             onBeforeAdd: onBeforeAdd,
  112.  
  113.             onAdd : onAdd,
  114.  
  115.             onUndo : onUndo,
  116.  
  117.             onRedo : onRedo,
  118.  
  119.             beforeChange : function() {
  120.                 beforeBookmark = editor.selection.getBookmark(2, true);
  121.             },
  122.  
  123.             add : function(level) {
  124.                 var i, settings = editor.settings, lastLevel;
  125.  
  126.                 level = level || {};
  127.                 level.content = getContent();
  128.                
  129.                 self.onBeforeAdd.dispatch(self, level);
  130.  
  131.                 // Add undo level if needed
  132.                 lastLevel = data[index];
  133.                 if (lastLevel && lastLevel.content == level.content)
  134.                     return null;
  135.  
  136.                 // Set before bookmark on previous level
  137.                 if (data[index])
  138.                     data[index].beforeBookmark = beforeBookmark;
  139.  
  140.                 // Time to compress
  141.                 if (settings.custom_undo_redo_levels) {
  142.                     if (data.length > settings.custom_undo_redo_levels) {
  143.                         for (i = 0; i < data.length - 1; i++)
  144.                             data[i] = data[i + 1];
  145.  
  146.                         data.length--;
  147.                         index = data.length;
  148.                     }
  149.                 }
  150.  
  151.                 // Get a non intrusive normalized bookmark
  152.                 level.bookmark = editor.selection.getBookmark(2, true);
  153.  
  154.                 // Crop array if needed
  155.                 if (index < data.length - 1)
  156.                     data.length = index + 1;
  157.  
  158.                 data.push(level);
  159.                 index = data.length - 1;
  160.  
  161.                 self.onAdd.dispatch(self, level);
  162.                 editor.isNotDirty = 0;
  163.  
  164.                 return level;
  165.             },
  166.  
  167.             undo : function() {
  168.                 var level, i;
  169.  
  170.                 if (self.typing) {
  171.                     self.add();
  172.                     self.typing = false;
  173.                 }
  174.  
  175.                 if (index > 0) {
  176.                     level = data[--index];
  177.  
  178.                     editor.setContent(level.content, {format : 'raw'});
  179.                     editor.selection.moveToBookmark(level.beforeBookmark);
  180.  
  181.                     self.onUndo.dispatch(self, level);
  182.                 }
  183.  
  184.                 return level;
  185.             },
  186.  
  187.             redo : function() {
  188.                 var level;
  189.  
  190.                 if (index < data.length - 1) {
  191.                     level = data[++index];
  192.  
  193.                     editor.setContent(level.content, {format : 'raw'});
  194.                     editor.selection.moveToBookmark(level.bookmark);
  195.  
  196.                     self.onRedo.dispatch(self, level);
  197.                 }
  198.  
  199.                 return level;
  200.             },
  201.  
  202.             clear : function() {
  203.                 data = [];
  204.                 index = 0;
  205.                 self.typing = false;
  206.             },
  207.  
  208.             hasUndo : function() {
  209.                 return index > 0 || this.typing;
  210.             },
  211.  
  212.             hasRedo : function() {
  213.                 return index < data.length - 1 && !this.typing;
  214.             }
  215.         };
  216.  
  217.         return self;
  218.     };
  219. })(tinymce);