text: /**
   * UndoManager.js
   *
   * Released under LGPL License.
   * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
   *
   * License: http://www.tinymce.com/license
   * Contributing: http://www.tinymce.com/contributing
   */
  
  /**
   * This class handles the undo/redo history levels for the editor. Since the built-in undo/redo has major drawbacks a custom one was needed.
   *
   * @class tinymce.UndoManager
   */
  define("tinymce/UndoManager", [
  	"tinymce/util/VK",
  	"tinymce/util/Tools",
  	"tinymce/undo/Levels",
  	"tinymce/Env"
  ], function(VK, Tools, Levels, Env) {
  	return function(editor) {
  		var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0;
  
  		function setDirty(state) {
  			editor.setDirty(state);
  		}
  
  		function addNonTypingUndoLevel(e) {
  			self.typing = false;
  			self.add({}, e);
  		}
  
  		function endTyping() {
  			if (self.typing) {
  				self.typing = false;
  				self.add();
  			}
  		}
  
  		// Add initial undo level when the editor is initialized
  		editor.on('init', function() {
  			self.add();
  		});
  
  		// Get position before an execCommand is processed
  		editor.on('BeforeExecCommand', function(e) {
  			var cmd = e.command;
  
  			if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') {
  				endTyping();
  				self.beforeChange();
  			}
  		});
  
  		// Add undo level after an execCommand call was made
  		editor.on('ExecCommand', function(e) {
  			var cmd = e.command;
  
  			if (cmd !== 'Undo' && cmd !== 'Redo' && cmd !== 'mceRepaint') {
  				addNonTypingUndoLevel(e);
  			}
  		});
  
  		editor.on('ObjectResizeStart Cut', function() {
  			self.beforeChange();
  		});
  
  		editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel);
  		editor.on('DragEnd', addNonTypingUndoLevel);
  
  		editor.on('KeyUp', function(e) {
  			var keyCode = e.keyCode;
  
  			// If key is prevented then don't add undo level
  			// This would happen on keyboard shortcuts for example
  			if (e.isDefaultPrevented()) {
  				return;
  			}
  
  			if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45 || e.ctrlKey) {
  				addNonTypingUndoLevel();
  				editor.nodeChanged();
  			}
  
  			if (keyCode === 46 || keyCode === 8 || (Env.mac && (keyCode === 91 || keyCode === 93))) {
  				editor.nodeChanged();
  			}
  
  			// Fire a TypingUndo event on the first character entered
  			if (isFirstTypedCharacter && self.typing) {
  				// Make it dirty if the content was changed after typing the first character
  				if (!editor.isDirty()) {
  					setDirty(data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0]));
  
  					// Fire initial change event
  					if (editor.isDirty()) {
  						editor.fire('change', {level: data[0], lastLevel: null});
  					}
  				}
  
  				editor.fire('TypingUndo');
  				isFirstTypedCharacter = false;
  				editor.nodeChanged();
  			}
  		});
  
  		editor.on('KeyDown', function(e) {
  			var keyCode = e.keyCode;
  
  			// If key is prevented then don't add undo level
  			// This would happen on keyboard shortcuts for example
  			if (e.isDefaultPrevented()) {
  				return;
  			}
  
  			// Is character position keys left,right,up,down,home,end,pgdown,pgup,enter
  			if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode === 45) {
  				if (self.typing) {
  					addNonTypingUndoLevel(e);
  				}
  
  				return;
  			}
  
  			// If key isn't Ctrl+Alt/AltGr
  			var modKey = (e.ctrlKey && !e.altKey) || e.metaKey;
  			if ((keyCode < 16 || keyCode > 20) && keyCode !== 224 && keyCode !== 91 && !self.typing && !modKey) {
  				self.beforeChange();
  				self.typing = true;
  				self.add({}, e);
  				isFirstTypedCharacter = true;
  			}
  		});
  
  		editor.on('MouseDown', function(e) {
  			if (self.typing) {
  				addNonTypingUndoLevel(e);
  			}
  		});
  
  		// Add keyboard shortcuts for undo/redo keys
  		editor.addShortcut('meta+z', '', 'Undo');
  		editor.addShortcut('meta+y,meta+shift+z', '', 'Redo');
  
  		editor.on('AddUndo Undo Redo ClearUndos', function(e) {
  			if (!e.isDefaultPrevented()) {
  				editor.nodeChanged();
  			}
  		});
  
  		/*eslint consistent-this:0 */
  		self = {
  			// Explode for debugging reasons
  			data: data,
  
  			/**
  			 * State if the user is currently typing or not. This will add a typing operation into one undo
  			 * level instead of one new level for each keystroke.
  			 *
  			 * @field {Boolean} typing
  			 */
  			typing: false,
  
  			/**
  			 * Stores away a bookmark to be used when performing an undo action so that the selection is before
  			 * the change has been made.
  			 *
  			 * @method beforeChange
  			 */
  			beforeChange: function() {
  				if (!locks) {
  					beforeBookmark = editor.selection.getBookmark(2, true);
  				}
  			},
  
  			/**
  			 * Adds a new undo level/snapshot to the undo list.
  			 *
  			 * @method add
  			 * @param {Object} level Optional undo level object to add.
  			 * @param {DOMEvent} event Optional event responsible for the creation of the undo level.
  			 * @return {Object} Undo level that got added or null it a level wasn't needed.
  			 */
  			add: function(level, event) {
  				var i, settings = editor.settings, lastLevel, currentLevel;
  
  				currentLevel = Levels.createFromEditor(editor);
  				level = level || {};
  				level = Tools.extend(level, currentLevel);
  
  				if (locks || editor.removed) {
  					return null;
  				}
  
  				lastLevel = data[index];
  				if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) {
  					return null;
  				}
  
  				// Add undo level if needed
  				if (lastLevel && Levels.isEq(lastLevel, level)) {
  					return null;
  				}
  
  				// Set before bookmark on previous level
  				if (data[index]) {
  					data[index].beforeBookmark = beforeBookmark;
  				}
  
  				// Time to compress
  				if (settings.custom_undo_redo_levels) {
  					if (data.length > settings.custom_undo_redo_levels) {
  						for (i = 0; i < data.length - 1; i++) {
  							data[i] = data[i + 1];
  						}
  
  						data.length--;
  						index = data.length;
  					}
  				}
  
  				// Get a non intrusive normalized bookmark
  				level.bookmark = editor.selection.getBookmark(2, true);
  
  				// Crop array if needed
  				if (index < data.length - 1) {
  					data.length = index + 1;
  				}
  
  				data.push(level);
  				index = data.length - 1;
  
  				var args = {level: level, lastLevel: lastLevel, originalEvent: event};
  
  				editor.fire('AddUndo', args);
  
  				if (index > 0) {
  					setDirty(true);
  					editor.fire('change', args);
  				}
  
  				return level;
  			},
  
  			/**
  			 * Undoes the last action.
  			 *
  			 * @method undo
  			 * @return {Object} Undo level or null if no undo was performed.
  			 */
  			undo: function() {
  				var level;
  
  				if (self.typing) {
  					self.add();
  					self.typing = false;
  				}
  
  				if (index > 0) {
  					level = data[--index];
  					Levels.applyToEditor(editor, level, true);
  					setDirty(true);
  					editor.fire('undo', {level: level});
  				}
  
  				return level;
  			},
  
  			/**
  			 * Redoes the last action.
  			 *
  			 * @method redo
  			 * @return {Object} Redo level or null if no redo was performed.
  			 */
  			redo: function() {
  				var level;
  
  				if (index < data.length - 1) {
  					level = data[++index];
  					Levels.applyToEditor(editor, level, false);
  					setDirty(true);
  					editor.fire('redo', {level: level});
  				}
  
  				return level;
  			},
  
  			/**
  			 * Removes all undo levels.
  			 *
  			 * @method clear
  			 */
  			clear: function() {
  				data = [];
  				index = 0;
  				self.typing = false;
  				self.data = data;
  				editor.fire('ClearUndos');
  			},
  
  			/**
  			 * Returns true/false if the undo manager has any undo levels.
  			 *
  			 * @method hasUndo
  			 * @return {Boolean} true/false if the undo manager has any undo levels.
  			 */
  			hasUndo: function() {
  				// Has undo levels or typing and content isn't the same as the initial level
  				return index > 0 || (self.typing && data[0] && !Levels.isEq(Levels.createFromEditor(editor), data[0]));
  			},
  
  			/**
  			 * Returns true/false if the undo manager has any redo levels.
  			 *
  			 * @method hasRedo
  			 * @return {Boolean} true/false if the undo manager has any redo levels.
  			 */
  			hasRedo: function() {
  				return index < data.length - 1 && !self.typing;
  			},
  
  			/**
  			 * Executes the specified mutator function as an undo transaction. The selection
  			 * before the modification will be stored to the undo stack and if the DOM changes
  			 * it will add a new undo level. Any methods within the translation that adds undo levels will
  			 * be ignored. So a translation can include calls to execCommand or editor.insertContent.
  			 *
  			 * @method transact
  			 * @param {function} callback Function that gets executed and has dom manipulation logic in it.
  			 * @return {Object} Undo level that got added or null it a level wasn't needed.
  			 */
  			transact: function(callback) {
  				endTyping();
  				self.beforeChange();
  
  				try {
  					locks++;
  					callback();
  				} finally {
  					locks--;
  				}
  
  				return self.add();
  			},
  
  			/**
  			 * Adds an extra "hidden" undo level by first applying the first mutation and store that to the undo stack
  			 * then roll back that change and do the second mutation on top of the stack. This will produce an extra
  			 * undo level that the user doesn't see until they undo.
  			 *
  			 * @method extra
  			 * @param {function} callback1 Function that does mutation but gets stored as a "hidden" extra undo level.
  			 * @param {function} callback2 Function that does mutation but gets displayed to the user.
  			 */
  			extra: function (callback1, callback2) {
  				var lastLevel, bookmark;
  
  				if (self.transact(callback1)) {
  					bookmark = data[index].bookmark;
  					lastLevel = data[index - 1];
  					Levels.applyToEditor(editor, lastLevel, true);
  
  					if (self.transact(callback2)) {
  						data[index - 1].beforeBookmark = bookmark;
  					}
  				}
  			}
  		};
  
  		return self;
  	};
  });
  