src/pyams_skin/resources/js/ext/tinymce/dev/classes/UndoManager.js
changeset 69 a361355b55c7
equal deleted inserted replaced
68:fd8fb93e1b6a 69:a361355b55c7
       
     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 /**
       
    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.
       
    13  *
       
    14  * @class tinymce.UndoManager
       
    15  */
       
    16 define("tinymce/UndoManager", [
       
    17 	"tinymce/util/VK",
       
    18 	"tinymce/Env",
       
    19 	"tinymce/util/Tools",
       
    20 	"tinymce/html/SaxParser"
       
    21 ], function(VK, Env, Tools, SaxParser) {
       
    22 	var trim = Tools.trim, trimContentRegExp;
       
    23 
       
    24 	trimContentRegExp = new RegExp([
       
    25 		'<span[^>]+data-mce-bogus[^>]+>[\u200B\uFEFF]+<\\/span>', // Trim bogus spans like caret containers
       
    26 		'\\s?data-mce-selected="[^"]+"' // Trim temporaty data-mce prefixed attributes like data-mce-selected
       
    27 	].join('|'), 'gi');
       
    28 
       
    29 	return function(editor) {
       
    30 		var self = this, index = 0, data = [], beforeBookmark, isFirstTypedCharacter, locks = 0;
       
    31 
       
    32 		/**
       
    33 		 * Returns a trimmed version of the editor contents to be used for the undo level. This
       
    34 		 * will remove any data-mce-bogus="all" marked elements since these are used for UI it will also
       
    35 		 * remove the data-mce-selected attributes used for selection of objects and caret containers.
       
    36 		 * It will keep all data-mce-bogus="1" elements since these can be used to place the caret etc and will
       
    37 		 * be removed by the serialization logic when you save.
       
    38 		 *
       
    39 		 * @private
       
    40 		 * @return {String} HTML contents of the editor excluding some internal bogus elements.
       
    41 		 */
       
    42 		function getContent() {
       
    43 			var content = editor.getContent({format: 'raw', no_events: 1});
       
    44 			var bogusAllRegExp = /<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g;
       
    45 			var endTagIndex, index, matchLength, matches, shortEndedElements, schema = editor.schema;
       
    46 
       
    47 			content = content.replace(trimContentRegExp, '');
       
    48 			shortEndedElements = schema.getShortEndedElements();
       
    49 
       
    50 			// Remove all bogus elements marked with "all"
       
    51 			while ((matches = bogusAllRegExp.exec(content))) {
       
    52 				index = bogusAllRegExp.lastIndex;
       
    53 				matchLength = matches[0].length;
       
    54 
       
    55 				if (shortEndedElements[matches[1]]) {
       
    56 					endTagIndex = index;
       
    57 				} else {
       
    58 					endTagIndex = SaxParser.findEndTag(schema, content, index);
       
    59 				}
       
    60 
       
    61 				content = content.substring(0, index - matchLength) + content.substring(endTagIndex);
       
    62 				bogusAllRegExp.lastIndex = index - matchLength;
       
    63 			}
       
    64 
       
    65 			return trim(content);
       
    66 		}
       
    67 
       
    68 		function setDirty(state) {
       
    69 			editor.isNotDirty = !state;
       
    70 		}
       
    71 
       
    72 		function addNonTypingUndoLevel(e) {
       
    73 			self.typing = false;
       
    74 			self.add({}, e);
       
    75 		}
       
    76 
       
    77 		// Add initial undo level when the editor is initialized
       
    78 		editor.on('init', function() {
       
    79 			self.add();
       
    80 		});
       
    81 
       
    82 		// Get position before an execCommand is processed
       
    83 		editor.on('BeforeExecCommand', function(e) {
       
    84 			var cmd = e.command;
       
    85 
       
    86 			if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') {
       
    87 				self.beforeChange();
       
    88 			}
       
    89 		});
       
    90 
       
    91 		// Add undo level after an execCommand call was made
       
    92 		editor.on('ExecCommand', function(e) {
       
    93 			var cmd = e.command;
       
    94 
       
    95 			if (cmd != 'Undo' && cmd != 'Redo' && cmd != 'mceRepaint') {
       
    96 				addNonTypingUndoLevel(e);
       
    97 			}
       
    98 		});
       
    99 
       
   100 		editor.on('ObjectResizeStart', function() {
       
   101 			self.beforeChange();
       
   102 		});
       
   103 
       
   104 		editor.on('SaveContent ObjectResized blur', addNonTypingUndoLevel);
       
   105 		editor.on('DragEnd', addNonTypingUndoLevel);
       
   106 
       
   107 		editor.on('KeyUp', function(e) {
       
   108 			var keyCode = e.keyCode;
       
   109 
       
   110 			if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45 || keyCode == 13 || e.ctrlKey) {
       
   111 				addNonTypingUndoLevel();
       
   112 				editor.nodeChanged();
       
   113 			}
       
   114 
       
   115 			if (keyCode == 46 || keyCode == 8 || (Env.mac && (keyCode == 91 || keyCode == 93))) {
       
   116 				editor.nodeChanged();
       
   117 			}
       
   118 
       
   119 			// Fire a TypingUndo event on the first character entered
       
   120 			if (isFirstTypedCharacter && self.typing) {
       
   121 				// Make it dirty if the content was changed after typing the first character
       
   122 				if (!editor.isDirty()) {
       
   123 					setDirty(data[0] && getContent() != data[0].content);
       
   124 
       
   125 					// Fire initial change event
       
   126 					if (!editor.isNotDirty) {
       
   127 						editor.fire('change', {level: data[0], lastLevel: null});
       
   128 					}
       
   129 				}
       
   130 
       
   131 				editor.fire('TypingUndo');
       
   132 				isFirstTypedCharacter = false;
       
   133 				editor.nodeChanged();
       
   134 			}
       
   135 		});
       
   136 
       
   137 		editor.on('KeyDown', function(e) {
       
   138 			var keyCode = e.keyCode;
       
   139 
       
   140 			// Is caracter positon keys left,right,up,down,home,end,pgdown,pgup,enter
       
   141 			if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 45) {
       
   142 				if (self.typing) {
       
   143 					addNonTypingUndoLevel(e);
       
   144 				}
       
   145 
       
   146 				return;
       
   147 			}
       
   148 
       
   149 			// If key isn't Ctrl+Alt/AltGr
       
   150 			var modKey = (e.ctrlKey && !e.altKey) || e.metaKey;
       
   151 			if ((keyCode < 16 || keyCode > 20) && keyCode != 224 && keyCode != 91 && !self.typing && !modKey) {
       
   152 				self.beforeChange();
       
   153 				self.typing = true;
       
   154 				self.add({}, e);
       
   155 				isFirstTypedCharacter = true;
       
   156 			}
       
   157 		});
       
   158 
       
   159 		editor.on('MouseDown', function(e) {
       
   160 			if (self.typing) {
       
   161 				addNonTypingUndoLevel(e);
       
   162 			}
       
   163 		});
       
   164 
       
   165 		// Add keyboard shortcuts for undo/redo keys
       
   166 		editor.addShortcut('meta+z', '', 'Undo');
       
   167 		editor.addShortcut('meta+y,meta+shift+z', '', 'Redo');
       
   168 
       
   169 		editor.on('AddUndo Undo Redo ClearUndos', function(e) {
       
   170 			if (!e.isDefaultPrevented()) {
       
   171 				editor.nodeChanged();
       
   172 			}
       
   173 		});
       
   174 
       
   175 		/*eslint consistent-this:0 */
       
   176 		self = {
       
   177 			// Explose for debugging reasons
       
   178 			data: data,
       
   179 
       
   180 			/**
       
   181 			 * State if the user is currently typing or not. This will add a typing operation into one undo
       
   182 			 * level instead of one new level for each keystroke.
       
   183 			 *
       
   184 			 * @field {Boolean} typing
       
   185 			 */
       
   186 			typing: false,
       
   187 
       
   188 			/**
       
   189 			 * Stores away a bookmark to be used when performing an undo action so that the selection is before
       
   190 			 * the change has been made.
       
   191 			 *
       
   192 			 * @method beforeChange
       
   193 			 */
       
   194 			beforeChange: function() {
       
   195 				if (!locks) {
       
   196 					beforeBookmark = editor.selection.getBookmark(2, true);
       
   197 				}
       
   198 			},
       
   199 
       
   200 			/**
       
   201 			 * Adds a new undo level/snapshot to the undo list.
       
   202 			 *
       
   203 			 * @method add
       
   204 			 * @param {Object} level Optional undo level object to add.
       
   205 			 * @param {DOMEvent} Event Optional event responsible for the creation of the undo level.
       
   206 			 * @return {Object} Undo level that got added or null it a level wasn't needed.
       
   207 			 */
       
   208 			add: function(level, event) {
       
   209 				var i, settings = editor.settings, lastLevel;
       
   210 
       
   211 				level = level || {};
       
   212 				level.content = getContent();
       
   213 
       
   214 				if (locks || editor.removed) {
       
   215 					return null;
       
   216 				}
       
   217 
       
   218 				lastLevel = data[index];
       
   219 				if (editor.fire('BeforeAddUndo', {level: level, lastLevel: lastLevel, originalEvent: event}).isDefaultPrevented()) {
       
   220 					return null;
       
   221 				}
       
   222 
       
   223 				// Add undo level if needed
       
   224 				if (lastLevel && lastLevel.content == level.content) {
       
   225 					return null;
       
   226 				}
       
   227 
       
   228 				// Set before bookmark on previous level
       
   229 				if (data[index]) {
       
   230 					data[index].beforeBookmark = beforeBookmark;
       
   231 				}
       
   232 
       
   233 				// Time to compress
       
   234 				if (settings.custom_undo_redo_levels) {
       
   235 					if (data.length > settings.custom_undo_redo_levels) {
       
   236 						for (i = 0; i < data.length - 1; i++) {
       
   237 							data[i] = data[i + 1];
       
   238 						}
       
   239 
       
   240 						data.length--;
       
   241 						index = data.length;
       
   242 					}
       
   243 				}
       
   244 
       
   245 				// Get a non intrusive normalized bookmark
       
   246 				level.bookmark = editor.selection.getBookmark(2, true);
       
   247 
       
   248 				// Crop array if needed
       
   249 				if (index < data.length - 1) {
       
   250 					data.length = index + 1;
       
   251 				}
       
   252 
       
   253 				data.push(level);
       
   254 				index = data.length - 1;
       
   255 
       
   256 				var args = {level: level, lastLevel: lastLevel, originalEvent: event};
       
   257 
       
   258 				editor.fire('AddUndo', args);
       
   259 
       
   260 				if (index > 0) {
       
   261 					setDirty(true);
       
   262 					editor.fire('change', args);
       
   263 				}
       
   264 
       
   265 				return level;
       
   266 			},
       
   267 
       
   268 			/**
       
   269 			 * Undoes the last action.
       
   270 			 *
       
   271 			 * @method undo
       
   272 			 * @return {Object} Undo level or null if no undo was performed.
       
   273 			 */
       
   274 			undo: function() {
       
   275 				var level;
       
   276 
       
   277 				if (self.typing) {
       
   278 					self.add();
       
   279 					self.typing = false;
       
   280 				}
       
   281 
       
   282 				if (index > 0) {
       
   283 					level = data[--index];
       
   284 
       
   285 					// Undo to first index then set dirty state to false
       
   286 					if (index === 0) {
       
   287 						setDirty(false);
       
   288 					}
       
   289 
       
   290 					editor.setContent(level.content, {format: 'raw'});
       
   291 					editor.selection.moveToBookmark(level.beforeBookmark);
       
   292 
       
   293 					editor.fire('undo', {level: level});
       
   294 				}
       
   295 
       
   296 				return level;
       
   297 			},
       
   298 
       
   299 			/**
       
   300 			 * Redoes the last action.
       
   301 			 *
       
   302 			 * @method redo
       
   303 			 * @return {Object} Redo level or null if no redo was performed.
       
   304 			 */
       
   305 			redo: function() {
       
   306 				var level;
       
   307 
       
   308 				if (index < data.length - 1) {
       
   309 					level = data[++index];
       
   310 
       
   311 					editor.setContent(level.content, {format: 'raw'});
       
   312 					editor.selection.moveToBookmark(level.bookmark);
       
   313 					setDirty(true);
       
   314 
       
   315 					editor.fire('redo', {level: level});
       
   316 				}
       
   317 
       
   318 				return level;
       
   319 			},
       
   320 
       
   321 			/**
       
   322 			 * Removes all undo levels.
       
   323 			 *
       
   324 			 * @method clear
       
   325 			 */
       
   326 			clear: function() {
       
   327 				data = [];
       
   328 				index = 0;
       
   329 				self.typing = false;
       
   330 				editor.fire('ClearUndos');
       
   331 			},
       
   332 
       
   333 			/**
       
   334 			 * Returns true/false if the undo manager has any undo levels.
       
   335 			 *
       
   336 			 * @method hasUndo
       
   337 			 * @return {Boolean} true/false if the undo manager has any undo levels.
       
   338 			 */
       
   339 			hasUndo: function() {
       
   340 				// Has undo levels or typing and content isn't the same as the initial level
       
   341 				return index > 0 || (self.typing && data[0] && getContent() != data[0].content);
       
   342 			},
       
   343 
       
   344 			/**
       
   345 			 * Returns true/false if the undo manager has any redo levels.
       
   346 			 *
       
   347 			 * @method hasRedo
       
   348 			 * @return {Boolean} true/false if the undo manager has any redo levels.
       
   349 			 */
       
   350 			hasRedo: function() {
       
   351 				return index < data.length - 1 && !this.typing;
       
   352 			},
       
   353 
       
   354 			/**
       
   355 			 * Executes the specified function in an undo transation. The selection
       
   356 			 * before the modification will be stored to the undo stack and if the DOM changes
       
   357 			 * it will add a new undo level. Any methods within the transation that adds undo levels will
       
   358 			 * be ignored. So a transation can include calls to execCommand or editor.insertContent.
       
   359 			 *
       
   360 			 * @method transact
       
   361 			 * @param {function} callback Function to execute dom manipulation logic in.
       
   362 			 */
       
   363 			transact: function(callback) {
       
   364 				self.beforeChange();
       
   365 
       
   366 				try {
       
   367 					locks++;
       
   368 					callback();
       
   369 				} finally {
       
   370 					locks--;
       
   371 				}
       
   372 
       
   373 				self.add();
       
   374 			}
       
   375 		};
       
   376 
       
   377 		return self;
       
   378 	};
       
   379 });